package org.github.jamm;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assume.assumeThat;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.ref.SoftReference;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.junit.Ignore;
import org.junit.Test;
/**
* Numbers here are for 64-bit Sun JVM. Good luck with anything else.
*/
public class MemoryMeterTest
{
// JVM memory structure is like follows
// a ref and a header for all objects
// always align to 8 byte boundary no matter what
// separate fields for each class in the family tree
// for a specific class, group: (depending on the jvm there may be optimized packing)
// - longs, doubles (aligned on 8 byte boundary)
// - ints, floats (aligned on 4 byte boundary)
// - shorts, chars (aligned on 2 byte boundary)
// - bytes, booleans (aligned on 1 byte boundary)
// - padding
// - last come refs
//
// for array, last comes the array body (refs or primitives)
//
// Mac OS X seems very good at packing and seems to use 4 byte references even on 64-bit!!!
static final int REFERENCE_SIZE = sizeOfReference();
static final int HEADER_SIZE = sizeOfHeader();
static final int OBJECT_SIZE = pad(HEADER_SIZE + REFERENCE_SIZE);
public static int sizeOfReference() {
MemoryMeter meter = new MemoryMeter();
// this is probably the safest way to get the reference size
// does imply that we are testing non-pure
// but there is so much variability, this at least will give
// consistent answers from an "approximate" data source
long objectArraySize = meter.measure(new Object[123]);
if (meter.measure(new long[123]) == objectArraySize) return 8;
if (meter.measure(new int[123]) == objectArraySize) return 4;
// OK there is strangeness going on... we'll ask some well known sources
try {
Class<?> unsafe = Class.forName("sun.misc.Unsafe");
Field unsafeField = unsafe.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
Object theUnsafe = unsafeField.get(null);
Method addressSize = unsafe.getMethod("addressSize");
Object size = addressSize.invoke(theUnsafe);
int i = ((Number) size).intValue();
return i;
} catch (ClassNotFoundException e) {
// ignore
} catch (NoSuchFieldException e) {
// ignore
} catch (IllegalAccessException e) {
// ignore
} catch (NoSuchMethodException e) {
// ignore
} catch (InvocationTargetException e) {
// ignore
}
String sadm = System.getProperty("sun.arch.data.model");
if (sadm != null) {
if ("32".equals(sadm)) return 4;
if ("64".equals(sadm)) return 8;
if ("128".equals(sadm)) return 16;
}
String oa = System.getProperty("os.arch");
if (oa != null) {
if (oa.contains("64")) return 8;
if (oa.contains("32")) return 4;
if ("x86".equals(oa)) return 4;
if ("amd".equals(oa)) return 4;
}
String jvi = System.getProperty("java.vm.info");
if (jvi != null) {
if (jvi.contains("x86-32")) return 4;
}
String jvn = System.getProperty("java.vm.name");
if (jvn != null) {
if (jvn.toLowerCase().contains("64-bit")) return 8;
if (jvn.toLowerCase().contains("32-bit")) return 4;
}
// ok we'll pretend it's 32-bit
return 4;
}
@SuppressWarnings("unused")
static class ByteHolder {
private byte value;
}
@SuppressWarnings("unused")
static class TwoByteHolder {
private byte value;
private byte overflow;
}
@SuppressWarnings("unused")
static class ThreeByteHolder {
private byte value;
private byte other1;
private byte overflow;
}
@SuppressWarnings("unused")
static class FourByteHolder {
private byte value;
private byte other1;
private byte other2;
private byte overflow;
}
@SuppressWarnings("unused")
static class FiveByteHolder {
private byte value;
private byte other1;
private byte other2;
private byte other3;
private byte overflow;
}
@SuppressWarnings("unused")
static class SixByteHolder {
private byte value;
private byte other1;
private byte other2;
private byte other3;
private byte overflow;
}
@SuppressWarnings("unused")
static class SevenByteHolder {
private byte value;
private byte other1;
private byte other2;
private byte other3;
private byte other4;
private byte other5;
private byte overflow;
}
@SuppressWarnings("unused")
static class EightByteHolder {
private byte value;
private byte other1;
private byte other2;
private byte other3;
private byte other4;
private byte other5;
private byte other6;
private byte overflow;
}
@SuppressWarnings("unused")
static class NineByteHolder {
private byte value;
private byte other1;
private byte other2;
private byte other3;
private byte other4;
private byte other5;
private byte other6;
private byte other7;
private byte overflow;
}
public static int sizeOfHeader() {
// The Mac seems to pack things right in!
MemoryMeter meter = new MemoryMeter();
long b0 = meter.measure(new Object()) - REFERENCE_SIZE;
long b1 = meter.measure(new ByteHolder()) - REFERENCE_SIZE;
long b2 = meter.measure(new TwoByteHolder()) - REFERENCE_SIZE;
long b3 = meter.measure(new ThreeByteHolder()) - REFERENCE_SIZE;
long b4 = meter.measure(new FourByteHolder()) - REFERENCE_SIZE;
long b5 = meter.measure(new FiveByteHolder()) - REFERENCE_SIZE;
long b6 = meter.measure(new SixByteHolder()) - REFERENCE_SIZE;
long b7 = meter.measure(new SevenByteHolder()) - REFERENCE_SIZE;
long b8 = meter.measure(new EightByteHolder()) - REFERENCE_SIZE;
long b9 = meter.measure(new NineByteHolder()) - REFERENCE_SIZE;
if (b0 == b8 && b0 != b9) return (int)b0 - 8;
if (b0 == b7 && b0 != b8) return (int)b0 - 7;
if (b0 == b6 && b0 != b7) return (int)b0 - 6;
if (b0 == b5 && b0 != b6) return (int)b0 - 5;
if (b0 == b4 && b0 != b5) return (int)b0 - 4;
if (b0 == b3 && b0 != b4) return (int)b0 - 3;
if (b0 == b2 && b0 != b3) return (int)b0 - 2;
if (b0 == b1 && b0 != b2) return (int)b0 - 1;
return (int)b0;
}
public static int pad(int size) {
if (size % 8 == 0) return size;
return (size | 7) + 1;
}
public static int objectSize(int numLongDoubles, int numIntFloats, int numShortChars, int numByteBools, int numRefs) {
int size = REFERENCE_SIZE + HEADER_SIZE;
size += numLongDoubles * 8;
size += numIntFloats * 4;
size += numShortChars * 2;
size += numByteBools;
// now pad to ref-size boundary
if (size % REFERENCE_SIZE != 0) {
size = (size | (REFERENCE_SIZE - 1)) + 1;
}
size += numRefs * REFERENCE_SIZE;
return pad(size);
}
public static int arraySize(int numElements) {
int size = REFERENCE_SIZE + HEADER_SIZE + 4;
if (numElements > 0) {
// now pad to ref-size boundary
if (size % REFERENCE_SIZE != 0) {
size = (size | (REFERENCE_SIZE - 1)) + 1;
}
size += numElements * REFERENCE_SIZE;
}
return pad(size);
}
public static int byteArraySize(int numElements) {
int size = REFERENCE_SIZE + HEADER_SIZE + 4;
if (numElements > 0) {
// now pad to ref-size boundary
if (size % REFERENCE_SIZE != 0) {
size = (size | (REFERENCE_SIZE - 1)) + 1;
}
size += numElements;
}
return pad(size);
}
public static int charArraySize(int numElements) {
int size = REFERENCE_SIZE + HEADER_SIZE + 4;
if (numElements > 0) {
// now pad to ref-size boundary
if (size % REFERENCE_SIZE != 0) {
size = (size | (REFERENCE_SIZE - 1)) + 1;
}
size += numElements * 2;
}
return pad(size);
}
public static int intArraySize(int numElements) {
int size = REFERENCE_SIZE + HEADER_SIZE + 4;
if (numElements > 0) {
// now pad to ref-size boundary
if (size % REFERENCE_SIZE != 0) {
size = (size | (REFERENCE_SIZE - 1)) + 1;
}
size += numElements * 4;
}
return pad(size);
}
public static int longArraySize(int numElements) {
int size = REFERENCE_SIZE + HEADER_SIZE + 4;
if (numElements > 0) {
// now pad to ref-size boundary
if (size % REFERENCE_SIZE != 0) {
size = (size | (REFERENCE_SIZE - 1)) + 1;
}
size += numElements * 8;
}
return pad(size);
}
@Test
public void testObjectArraySizes() {
MemoryMeter meter = new MemoryMeter();
assertEquals("Shallow size of Object[0]", arraySize(0), meter.measure(new Object[0]));
assertEquals("Shallow size of Object[1]", arraySize(1), meter.measure(new Object[1]));
assertEquals("Shallow size of Object[256]", arraySize(256), meter.measure(new Object[256]));
}
@Test
public void testByteArraySizes() {
MemoryMeter meter = new MemoryMeter();
assertEquals("Shallow size of byte[0]", byteArraySize(0), meter.measure(new byte[0]));
assertEquals("Shallow size of byte[1]", byteArraySize(1), meter.measure(new byte[1]));
assertEquals("Shallow size of byte[256]", byteArraySize(256), meter.measure(new byte[256]));
}
@Test
public void testCharArraySizes() {
MemoryMeter meter = new MemoryMeter();
assertEquals("Shallow size of char[0]", charArraySize(0), meter.measure(new char[0]));
assertEquals("Shallow size of char[1]", charArraySize(1), meter.measure(new char[1]));
assertEquals("Shallow size of char[256]", charArraySize(256), meter.measure(new char[256]));
}
@Test
public void testIntArraySizes() {
MemoryMeter meter = new MemoryMeter();
assertEquals("Shallow size of int[0]", intArraySize(0), meter.measure(new int[0]));
assertEquals("Shallow size of int[1]", intArraySize(1), meter.measure(new int[1]));
assertEquals("Shallow size of int[256]", intArraySize(256), meter.measure(new int[256]));
}
@Test
public void testLongArraySizes() {
MemoryMeter meter = new MemoryMeter();
assertEquals("Shallow size of long[0]", longArraySize(0), meter.measure(new long[0]));
assertEquals("Shallow size of long[1]", longArraySize(1), meter.measure(new long[1]));
assertEquals("Shallow size of long[256]", longArraySize(256), meter.measure(new long[256]));
}
@SuppressWarnings("unused")
static class LongHolder {
private long value;
}
@SuppressWarnings("unused")
static class IntHolder {
private int value;
}
@SuppressWarnings("unused")
static class CharHolder {
private char value;
}
@SuppressWarnings("unused")
static class TwoCharHolder {
private char value;
private char other;
}
@SuppressWarnings("unused")
static class ThreeCharHolder {
private char value;
private char other;
private char overflow;
}
@SuppressWarnings("unused")
static class IntCharHolder {
private int value;
private char other;
}
@SuppressWarnings("unused")
static class LongIntHolder {
private long value;
private int other;
}
@SuppressWarnings("unused")
static class LongIntHolder2 extends LongHolder {
private int other;
}
@Test
public void testMacOSX_x86_64() {
// Mac OS X seems to have a way to stash 4 bytes away in a plain object
MemoryMeter meter = new MemoryMeter();
assumeThat(System.getProperty("os.name"), is("Mac OS X"));
assumeThat(System.getProperty("os.arch"), is("x86_64"));
assertEquals("no embedded long field", 24, meter.measure(new LongHolder()));
assertEquals("Embedded int field", 16, meter.measure(new IntHolder()));
assertEquals("Embedded char field", 16, meter.measure(new CharHolder()));
assertEquals("Embedded char field * 2", 16, meter.measure(new TwoCharHolder()));
assertEquals("Embedded char field * 3", 24, meter.measure(new ThreeCharHolder()));
assertEquals("Embedded int field only", 24, meter.measure(new IntCharHolder()));
assertEquals("Only 4 bytes available", 24, meter.measure(new FiveByteHolder()));
assertEquals("4 bytes always available", 24, meter.measure(new LongIntHolder()));
assertEquals("4 bytes not available if parent has a field", 32, meter.measure(new LongIntHolder2()));
assertEquals(meter.measure(new int[16384]), meter.measure(new Object[16384]));
}
@Test
public void testMacOSX_i386() {
// Mac OS X seems to have a way to stash 4 bytes away in a plain object
MemoryMeter meter = new MemoryMeter();
assumeThat(System.getProperty("os.name"), is("Mac OS X"));
assumeThat(System.getProperty("os.arch"), is("i386"));
assertEquals("Room for 1 long", 16, meter.measure(new LongHolder()));
assertEquals("Room for 1 int", 16, meter.measure(new IntHolder()));
assertEquals("Room for 1 char", 16, meter.measure(new CharHolder()));
assertEquals("Room for 2 chars", 16, meter.measure(new TwoCharHolder()));
assertEquals("Room for 3 chars", 16, meter.measure(new ThreeCharHolder()));
assertEquals("Room for 1 int and 1 char", 16, meter.measure(new IntCharHolder()));
assertEquals("Room for 5 bytes", 16, meter.measure(new FiveByteHolder()));
assertEquals("Room for 7 bytes", 16, meter.measure(new SevenByteHolder()));
assertEquals("Room for 8 bytes", 16, meter.measure(new EightByteHolder()));
assertEquals("Room for 9 bytes", 24, meter.measure(new NineByteHolder()));
assertEquals("Room for 1 long and 1 int", 24, meter.measure(new LongIntHolder()));
assertEquals("4 bytes not available to child classes", 24, meter.measure(new LongIntHolder2()));
assertEquals(meter.measure(new int[16384]), meter.measure(new Object[16384]));
}
@Test
public void testPrimitives() {
MemoryMeter meter = new MemoryMeter();
assertEquals("Shallow size of Object", OBJECT_SIZE, meter.measure(new Object()));
assertEquals("Deep size of Object", OBJECT_SIZE, meter.measureDeep(new Object()));
assertEquals("Shallow size of Long", objectSize(1, 0, 0, 0, 0), meter.measure(new Long(0)));
assertEquals("Deep size of Long", objectSize(1, 0, 0, 0, 0), meter.measureDeep(new Long(0)));
assertEquals("Shallow size of Integer", objectSize(0, 1, 0, 0, 0), meter.measure(new Integer(0)));
assertEquals("Deep size of Integer", objectSize(0, 1, 0, 0, 0), meter.measureDeep(new Integer(0)));
assertEquals("Shallow size of empty String", objectSize(0, 0, 4, 0, 0), meter.measure(""));
assertEquals("Deep size of empty String", objectSize(0, 0, 4, 0, 0) + charArraySize(0), meter.measureDeep(""));
assertEquals("Shallow size of one-character String", objectSize(0, 0, 4, 0, 0), meter.measure("a"));
assertEquals("Deep size of one-character String", objectSize(0, 0, 4, 0, 0) + charArraySize(1), meter.measureDeep("a"));
assertEquals("Shallow size of empty array of objects", arraySize(0), meter.measure(new Object[0]));
Object[] objects = new Object[100];
assertEquals("Shallow size of Object[100] containing all nulls", arraySize(100), meter.measure(objects));
assertEquals("Deep size of Object[100] containing all nulls", arraySize(100), meter.measureDeep(objects));
for(int i = 0; i < objects.length; i++) {
objects[i] = new Object();
}
assertEquals("Shallow size of Object[100] containing new Object()s", arraySize(100) + OBJECT_SIZE * 100, meter.measureDeep(objects));
}
@Test
public void testByteBuffer() {
ByteBuffer empty = ByteBuffer.allocate(0);
ByteBuffer one = ByteBuffer.allocate(1);
ByteBuffer emptyOne = (ByteBuffer) one.duplicate().position(1);
MemoryMeter m1 = new MemoryMeter();
MemoryMeter m2 = m1.omitSharedBufferOverhead();
// from Object
// ref*2
// from Buffer
// long
// int * 4
// from ByteBuffer
// int
// boolean * 3
// padding
// byte[]
int BYTEBUFFER_SIZE = objectSize(1, 5, 0, 3, 1);
assertEquals("Shallow empty ByteBuffer", BYTEBUFFER_SIZE, m1.measure(empty));
assertEquals("Deep empty ByteBuffer", BYTEBUFFER_SIZE + byteArraySize(0), m1.measureDeep(empty));
// 8 is apparently the minimum number of bytes allocated
assertEquals("Deep 1-byte ByteBuffer", BYTEBUFFER_SIZE + byteArraySize(1), m1.measureDeep(one));
assertEquals("Deep duplicated 1-byte ByteBuffer", BYTEBUFFER_SIZE + byteArraySize(1), m1.measureDeep(emptyOne));
// there are hard-coded 64-bit arch values when omiting shared buffer overhead
assertEquals(BYTEBUFFER_SIZE, m2.measure(empty));
assertEquals(BYTEBUFFER_SIZE, m2.measureDeep(empty));
assertEquals(BYTEBUFFER_SIZE + 1, m2.measureDeep(one)); // as of 0.2.4 we don't count the bytes!!!
assertEquals(BYTEBUFFER_SIZE, m2.measureDeep(emptyOne));
}
@Test
public void testCycle() throws Exception {
MemoryMeter meter = new MemoryMeter();
Recursive dummy = new Recursive();
assertEquals("Shallow size of Recursive object", objectSize(0, 1, 0, 0, 1), meter.measure(dummy));
assertEquals("Deep size of Recursive is shallow size when child==null", meter.measure(dummy), meter.measureDeep(dummy));
dummy.child = dummy;
assertEquals("Deep size of Recursive is shallow size when child==this", meter.measure(dummy), meter.measureDeep(dummy));
}
@Test
public void testInheritance() {
MemoryMeter meter = new MemoryMeter();
assertEquals("Shallow size of Parent", objectSize(0, 1, 0, 0, 0), meter.measure(new Parent()));
assertEquals("Deep size of Parent", objectSize(0, 1, 0, 0, 0), meter.measureDeep(new Parent()));
assertEquals("Shallow size of Child", objectSize(0, 2, 0, 0, 0), meter.measure(new Child()));
assertEquals("Deep size of Parent", objectSize(0, 2, 0, 0, 0), meter.measureDeep(new Child()));
}
@Test
@Ignore("These vary quite radically depending on the JVM.")
public void testCollections() {
MemoryMeter meter = new MemoryMeter();
assertEquals("sizeOf ArrayList",
objectSize(0, 2, 0, 0, 1) // the object itself
+ arraySize(10), // the backing array's initial load factor
meter.measureDeep(new ArrayList<Object>()));
assertEquals("sizeOf HashMap",
objectSize(0, 4, 0, 0, 4) // the object itself
+ arraySize(16), // the backing array
meter.measureDeep(new HashMap<Object, Object>()));
assertEquals("sizeOf LinkedHashMap",
objectSize(0, 4, 0, 1, 5) // the object itself
+ arraySize(16) // the inherited backing array
+ objectSize(0, 1, 0, 0, 5), // the first node
meter.measureDeep(new LinkedHashMap<Object, Object>()));
// I give up for the ones below!
assertEquals("sizeOf ReentrantReadWriteLock", 176, meter.measureDeep(new ReentrantReadWriteLock()));
assertEquals("sizeOf ConcurrentSkipListMap", 192, meter.measureDeep(new ConcurrentSkipListMap<Object, Object>()));
}
@Test
public void testDeep() {
MemoryMeter meter = new MemoryMeter();
Recursive root = new Recursive();
Recursive recursive = root;
for (int i = 0; i < 100000; i++) {
recursive.child = new Recursive();
recursive = recursive.child;
}
assertEquals(objectSize(0, 1, 0, 0, 1) * 100001, meter.measureDeep(root));
}
@SuppressWarnings("unused")
private static class Parent {
private int i;
}
@SuppressWarnings("unused")
private static class Child extends Parent {
private int j;
}
@SuppressWarnings("unused")
private static class Recursive {
int i;
Recursive child = null;
}
@Test
public void testIgnoreKnownSingletons() {
MemoryMeter meter = new MemoryMeter();
long classFieldSize = meter.measureDeep(new HasClassField());
long enumFieldSize = meter.measureDeep(new HasEnumField());
meter = meter.ignoreKnownSingletons();
assertNotEquals(classFieldSize, meter.measureDeep(new HasClassField()));
assertNotEquals(enumFieldSize, meter.measureDeep(new HasEnumField()));
}
@Test
public void testIgnoreNonStrongReferences() {
MemoryMeter meter = new MemoryMeter();
long classFieldSize = meter.measureDeep(new HasReferenceField());
meter = meter.ignoreNonStrongReferences();
assertNotEquals(classFieldSize, meter.measureDeep(new HasClassField()));
}
@SuppressWarnings("unused")
private static class HasClassField {
private Class<?> cls = String.class;
}
@SuppressWarnings("unused")
private static class HasEnumField {
enum Giant {Fee, Fi, Fo, Fum}
private Giant grunt = Giant.Fee;
}
@SuppressWarnings("unused")
private static class HasReferenceField {
private SoftReference<Date> ref = new SoftReference<Date>(new Date());
}
@Test
public void testUnmeteredAnnotationOnFields() {
MemoryMeter meter = new MemoryMeter();
String s = "test";
long stringSize = meter.measureDeep(s);
long withoutSize = meter.measureDeep(new WithoutAnnotationField(null));
assertEquals(stringSize + withoutSize, meter.measureDeep(new WithoutAnnotationField(s)));
long withSize = meter.measureDeep(new WithAnnotationField(null));
assertEquals(withSize, meter.measureDeep(new WithAnnotationField(s)));
}
@Test
public void testUnmeteredTypeAnnotation() {
MemoryMeter meter = new MemoryMeter();
String s = "test";
assertEquals(0, meter.measureDeep(new WithTypeAnnotation(s)));
}
@Test
public void testUnmeteredAnnotationOnParent() {
MemoryMeter meter = new MemoryMeter();
String s = "test";
assertEquals(0, meter.measureDeep(new WithParentWithAnnotation(s)));
}
@Test
public void testUnmeteredAnnotationOnFieldParent() {
MemoryMeter meter = new MemoryMeter();
long withoutSize = meter.measureDeep(new WithFieldWithAnnotatedParent(null));
WithParentWithAnnotation field = new WithParentWithAnnotation("test");
long withSize = meter.measureDeep(new WithFieldWithAnnotatedParent(field));
assertEquals(withoutSize, withSize);
}
@Test
public void testUnmeteredAnnotationOnFieldInterface() {
MemoryMeter meter = new MemoryMeter();
long withoutSize = meter.measureDeep(new WithFieldAnnotatedInterface(null));
AnnotatedInterface field = new AnnotatedInterface() {
};
long withSize = meter.measureDeep(new WithFieldAnnotatedInterface(field));
assertEquals(withoutSize, withSize);
}
@SuppressWarnings("unused")
private static class WithoutAnnotationField {
private String s;
public WithoutAnnotationField(String s) {
this.s = s;
}
}
private static class WithAnnotationField {
@org.github.jamm.Unmetered
private String s;
public WithAnnotationField(String s) {
this.s = s;
}
}
@Unmetered
@SuppressWarnings("unused")
private static class WithTypeAnnotation {
private String s;
public WithTypeAnnotation(String s) {
this.s = s;
}
}
private static class WithParentWithAnnotation extends WithTypeAnnotation {
public WithParentWithAnnotation(String s) {
super(s);
}
}
@SuppressWarnings("unused")
private static class WithFieldWithAnnotatedParent {
private final WithParentWithAnnotation field;
public WithFieldWithAnnotatedParent(WithParentWithAnnotation field) {
this.field = field;
}
}
@Unmetered
private interface AnnotatedInterface {
}
@SuppressWarnings("unused")
private static class WithFieldAnnotatedInterface {
private final AnnotatedInterface field;
public WithFieldAnnotatedInterface(AnnotatedInterface field) {
this.field = field;
}
}
}