這篇說說如何計(jì)算Java對(duì)象大小的方法。之前在聊聊高并發(fā)(四)Java對(duì)象的表示模型和運(yùn)行時(shí)內(nèi)存表示 這篇中已經(jīng)說了Java對(duì)象的內(nèi)存表示模型是Oop-Klass模型。
普通對(duì)象的結(jié)構(gòu)如下,按64位機(jī)器的長度計(jì)算
1. 對(duì)象頭(_mark), 8個(gè)字節(jié)
2. Oop指針,如果是32G內(nèi)存以下的,默認(rèn)開啟對(duì)象指針壓縮,4個(gè)字節(jié)
3. 數(shù)據(jù)區(qū)
4.Padding(內(nèi)存對(duì)齊),按照8的倍數(shù)對(duì)齊
數(shù)組對(duì)象結(jié)構(gòu)是
1. 對(duì)象頭(_mark), 8個(gè)字節(jié)
2. Oop指針,如果是32G內(nèi)存以下的,默認(rèn)開啟對(duì)象指針壓縮,4個(gè)字節(jié)
3. 數(shù)組長度,4個(gè)字節(jié)
4. 數(shù)據(jù)區(qū)
5. Padding(內(nèi)存對(duì)齊),按照8的倍數(shù)對(duì)齊
清楚了對(duì)象在內(nèi)存的基本布局后,咱們說兩種計(jì)算Java對(duì)象大小的方法
1. 通過java.lang.instrument.Instrumentation的getObjectSize(obj)直接獲取對(duì)象的大小
2. 通過sun.misc.Unsafe對(duì)象的objectFieldOffset(field)等方法結(jié)合反射來計(jì)算對(duì)象的大小
java.lang.instrument.Instrumentation.getObjectSize()的方式
先講講java.lang.instrument.Instrumentation.getObjectSize()的方式,這種方法得到的是Shallow Size,即遇到引用時(shí),只計(jì)算引用的長度,不計(jì)算所引用的對(duì)象的實(shí)際大小。如果要計(jì)算所引用對(duì)象的實(shí)際大小,可以通過遞歸的方式去計(jì)算。
java.lang.instrument.Instrumentation的實(shí)例必須通過指定javaagent的方式才能獲得,具體的步驟如下:
1. 定義一個(gè)類,提供一個(gè)premain方法: public static void premain(String agentArgs, Instrumentation instP)
2. 創(chuàng)建META-INF/MANIFEST.MF文件,內(nèi)容是指定PreMain的類是哪個(gè): Premain-Class: sizeof.ObjectShallowSize
3. 把這個(gè)類打成jar,然后用java -javaagent XXXX.jar XXX.main的方式執(zhí)行
下面先定義一個(gè)類來獲得java.lang.instrument.Instrumentation的實(shí)例,并提供了一個(gè)static的sizeOf方法對(duì)外提供Instrumentation的能力
- package sizeof;
-
- import java.lang.instrument.Instrumentation;
-
- public class ObjectShallowSize {
- private static Instrumentation inst;
-
- public static void premain(String agentArgs, Instrumentation instP){
- inst = instP;
- }
-
- public static long sizeOf(Object obj){
- return inst.getObjectSize(obj);
- }
- }
定義META-INF/MANIFEST.MF文件
- Premain-Class: sizeof.ObjectShallowSize
打成jar包
- cd 編譯后的類和META-INF文件夾所在目錄
- jar cvfm java-agent-sizeof.jar META-INF/MANIFEST.MF .
準(zhǔn)備好了這個(gè)jar之后,我們可以寫測試類來測試Instrumentation的getObjectSize方法了。在這之前我們先來看對(duì)象在內(nèi)存中是按照什么順序排列的
有如下這個(gè)類,字段的定義按如下順序
- private static class ObjectA {
- String str; // 4
- int i1; // 4
- byte b1; // 1
- byte b2; // 1
- int i2; // 4
- ObjectB obj; //4
- byte b3; // 1
- }
按照我們之前說的方法來計(jì)算一下這個(gè)對(duì)象所占大小,注意按8對(duì)齊
8(_mark) + 4(oop指針) + 4(str) + 4(i1) + 1(b1) + 1(b2) + 2(padding) + 4(i2) + 4(obj) + 1(b3) + 7(padding) = 40 ?
但事實(shí)上是這樣的嗎? 我們來用Instrumentation的getObjectSize來計(jì)算一下先:
- package test;
-
- import sizeof.ObjectShallowSize;
-
- public class SizeofWithInstrumetation {
- private static class ObjectA {
- String str; // 4
- int i1; // 4
- byte b1; // 1
- byte b2; // 1
- int i2; // 4
- ObjectB obj; //4
- byte b3; // 1
- }
-
- private static class ObjectB {
-
- }
-
- public static void main(String[] args){
- System.out.println(ObjectShallowSize.sizeOf(new ObjectA()));
- }
- }
得到的結(jié)果是32!不是會(huì)按8對(duì)齊嗎,b3之前的數(shù)據(jù)加起來已經(jīng)是32了,多了1個(gè)b3,為33,應(yīng)該對(duì)齊到40才對(duì)啊。事實(shí)上,HotSpot創(chuàng)建的對(duì)象的字段會(huì)先按照給定順序排列一下,默認(rèn)的順序如下,從長到短排列,引用排最后: long/double --> int/float --> short/char --> byte/boolean --> Reference
這個(gè)順序可以使用JVM參數(shù): -XX:FieldsAllocationSylte=0(默認(rèn)是1)來改變。
我們使用sun.misc.Unsafe對(duì)象的objectFieldOffset方法來驗(yàn)證一下:
- Field[] fields = ObjectA.class.getDeclaredFields();
- for(Field f: fields){
- System.out.println(f.getName() + " offset: " +unsafe.objectFieldOffset(f));
- }
可以看到確實(shí)是按照從長到短,引用排最后的方式在內(nèi)存中排列的。按照這種方法我們來重新計(jì)算下ObjectA創(chuàng)建的對(duì)象的長度:
8(_mark) + 4(oop指針) + 4(i1) + + 4(i2) + 1(b1) + 1(b2) + 1(b3) + 1(padding) + 4(str) + 4(obj) = 32
得到的結(jié)果和java.lang.instrument.Instrumentation.getObjectSize()的結(jié)果是一樣的,證明我們的計(jì)算方式是正確的。
sun.misc.Unsafe的方式
下面說一下通過sun.misc.Unsafe對(duì)象的objectFieldOffset(field)等方法結(jié)合反射來計(jì)算對(duì)象的大小。基本的思路如下:
1. 通過反射獲得一個(gè)類的Field
2. 通過Unsafe的objectFieldOffset()獲得每個(gè)Field的offSet
3. 對(duì)Field按照offset排序,取得最大的offset,然后加上這個(gè)field的長度,再加上Padding對(duì)齊
上面三步就可以獲得一個(gè)對(duì)象的Shallow size。可以進(jìn)一步通過遞歸去計(jì)算所引用對(duì)象的大小,從而可以計(jì)算出一個(gè)對(duì)象所占用的實(shí)際大小。
如何獲得Unsafe對(duì)象已經(jīng)在這篇中聊聊序列化(二)使用sun.misc.Unsafe繞過new機(jī)制來創(chuàng)建Java對(duì)象說過了,可以通過反射的機(jī)制來獲得.
Oop指針是4還是未壓縮的8也可以通過unsafe.arrayIndexScale(Object[].class)來獲得,這個(gè)方法返回一個(gè)引用所占用的長度
- static {
- try {
- Field field = Unsafe.class.getDeclaredField("theUnsafe");
- field.setAccessible(true);
- unsafe = (Unsafe) field.get(null);
-
- objectRefSize = unsafe.arrayIndexScale(Object[].class);
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- }
下面的源碼摘自 http://java-performance.info/memory-introspection-using-sun-misc-unsafe-and-reflection/, 原文中的代碼在計(jì)算對(duì)象大小的時(shí)候有問題,我做了微調(diào),并加上了內(nèi)存對(duì)齊的方法,這樣計(jì)算出的結(jié)果和Instrumentation的getObjectSize方法是一樣的。
- package test;
-
- import java.util.ArrayList;
- import java.util.Collections;
- import java.util.Comparator;
- import java.util.List;
-
- /**
- * This class contains object info generated by ClassIntrospector tool
- */
- public class ObjectInfo {
- /** Field name */
- public final String name;
- /** Field type name */
- public final String type;
- /** Field data formatted as string */
- public final String contents;
- /** Field offset from the start of parent object */
- public final int offset;
- /** Memory occupied by this field */
- public final int length;
- /** Offset of the first cell in the array */
- public final int arrayBase;
- /** Size of a cell in the array */
- public final int arrayElementSize;
- /** Memory occupied by underlying array (shallow), if this is array type */
- public final int arraySize;
- /** This object fields */
- public final List<ObjectInfo> children;
-
- public ObjectInfo(String name, String type, String contents, int offset, int length, int arraySize,
- int arrayBase, int arrayElementSize)
- {
- this.name = name;
- this.type = type;
- this.contents = contents;
- this.offset = offset;
- this.length = length;
- this.arraySize = arraySize;
- this.arrayBase = arrayBase;
- this.arrayElementSize = arrayElementSize;
- children = new ArrayList<ObjectInfo>( 1 );
- }
-
- public void addChild( final ObjectInfo info )
- {
- if ( info != null )
- children.add( info );
- }
-
- /**
- * Get the full amount of memory occupied by a given object. This value may be slightly less than
- * an actual value because we don't worry about memory alignment - possible padding after the last object field.
- *
- * The result is equal to the last field offset + last field length + all array sizes + all child objects deep sizes
- * @return Deep object size
- */
- public long getDeepSize()
- {
- //return length + arraySize + getUnderlyingSize( arraySize != 0 );
- return addPaddingSize(arraySize + getUnderlyingSize( arraySize != 0 ));
- }
-
- long size = 0;
-
- private long getUnderlyingSize( final boolean isArray )
- {
- //long size = 0;
- for ( final ObjectInfo child : children )
- size += child.arraySize + child.getUnderlyingSize( child.arraySize != 0 );
- if ( !isArray && !children.isEmpty() ){
- int tempSize = children.get( children.size() - 1 ).offset + children.get( children.size() - 1 ).length;
- size += addPaddingSize(tempSize);
- }
-
- return size;
- }
-
- private static final class OffsetComparator implements Comparator<ObjectInfo>
- {
- @Override
- public int compare( final ObjectInfo o1, final ObjectInfo o2 )
- {
- return o1.offset - o2.offset; //safe because offsets are small non-negative numbers
- }
- }
-
- //sort all children by their offset
- public void sort()
- {
- Collections.sort( children, new OffsetComparator() );
- }
-
- @Override
- public String toString() {
- final StringBuilder sb = new StringBuilder();
- toStringHelper( sb, 0 );
- return sb.toString();
- }
-
- private void toStringHelper( final StringBuilder sb, final int depth )
- {
- depth( sb, depth ).append("name=").append( name ).append(", type=").append( type )
- .append( ", contents=").append( contents ).append(", offset=").append( offset )
- .append(", length=").append( length );
- if ( arraySize > 0 )
- {
- sb.append(", arrayBase=").append( arrayBase );
- sb.append(", arrayElemSize=").append( arrayElementSize );
- sb.append( ", arraySize=").append( arraySize );
- }
- for ( final ObjectInfo child : children )
- {
- sb.append( '\n' );
- child.toStringHelper(sb, depth + 1);
- }
- }
-
- private StringBuilder depth( final StringBuilder sb, final int depth )
- {
- for ( int i = 0; i < depth; ++i )
- sb.append( "\t");
- return sb;
- }
-
- private long addPaddingSize(long size){
- if(size % 8 != 0){
- return (size / 8 + 1) * 8;
- }
- return size;
- }
-
- }
-
-
- package test;
-
- import java.lang.reflect.Array;
- import java.lang.reflect.Field;
- import java.lang.reflect.Modifier;
- import java.util.ArrayList;
- import java.util.Arrays;
- import java.util.Collections;
- import java.util.HashMap;
- import java.util.IdentityHashMap;
- import java.util.List;
- import java.util.Map;
-
- import sun.misc.Unsafe;
-
- /**
- * This class could be used for any object contents/memory layout printing.
- */
- public class ClassIntrospector {
-
- private static final Unsafe unsafe;
- /** Size of any Object reference */
- private static final int objectRefSize;
- static {
- try {
- Field field = Unsafe.class.getDeclaredField("theUnsafe");
- field.setAccessible(true);
- unsafe = (Unsafe) field.get(null);
-
- objectRefSize = unsafe.arrayIndexScale(Object[].class);
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- }
-
- /** Sizes of all primitive values */
- private static final Map<Class, Integer> primitiveSizes;
-
- static {
- primitiveSizes = new HashMap<Class, Integer>(10);
- primitiveSizes.put(byte.class, 1);
- primitiveSizes.put(char.class, 2);
- primitiveSizes.put(int.class, 4);
- primitiveSizes.put(long.class, 8);
- primitiveSizes.put(float.class, 4);
- primitiveSizes.put(double.class, 8);
- primitiveSizes.put(boolean.class, 1);
- }
-
- /**
- * Get object information for any Java object. Do not pass primitives to
- * this method because they will boxed and the information you will get will
- * be related to a boxed version of your value.
- *
- * @param obj
- * Object to introspect
- * @return Object info
- * @throws IllegalAccessException
- */
- public ObjectInfo introspect(final Object obj)
- throws IllegalAccessException {
- try {
- return introspect(obj, null);
- } finally { // clean visited cache before returning in order to make
- // this object reusable
- m_visited.clear();
- }
- }
-
- // we need to keep track of already visited objects in order to support
- // cycles in the object graphs
- private IdentityHashMap<Object, Boolean> m_visited = new IdentityHashMap<Object, Boolean>(
- 100);
-
- private ObjectInfo introspect(final Object obj, final Field fld)
- throws IllegalAccessException {
- // use Field type only if the field contains null. In this case we will
- // at least know what's expected to be
- // stored in this field. Otherwise, if a field has interface type, we
- // won't see what's really stored in it.
- // Besides, we should be careful about primitives, because they are
- // passed as boxed values in this method
- // (first arg is object) - for them we should still rely on the field
- // type.
- boolean isPrimitive = fld != null && fld.getType().isPrimitive();
- boolean isRecursive = false; // will be set to true if we have already
- // seen this object
- if (!isPrimitive) {
- if (m_visited.containsKey(obj))
- isRecursive = true;
- m_visited.put(obj, true);
- }
-
- final Class type = (fld == null || (obj != null && !isPrimitive)) ? obj
- .getClass() : fld.getType();
- int arraySize = 0;
- int baseOffset = 0;
- int indexScale = 0;
- if (type.isArray() && obj != null) {
- baseOffset = unsafe.arrayBaseOffset(type);
- indexScale = unsafe.arrayIndexScale(type);
- arraySize = baseOffset + indexScale * Array.getLength(obj);
- }
-
- final ObjectInfo root;
- if (fld == null) {
- root = new ObjectInfo("", type.getCanonicalName(), getContents(obj,
- type), 0, getShallowSize(type), arraySize, baseOffset,
- indexScale);
- } else {
- final int offset = (int) unsafe.objectFieldOffset(fld);
- root = new ObjectInfo(fld.getName(), type.getCanonicalName(),
- getContents(obj, type), offset, getShallowSize(type),
- arraySize, baseOffset, indexScale);
- }
-
- if (!isRecursive && obj != null) {
- if (isObjectArray(type)) {
- // introspect object arrays
- final Object[] ar = (Object[]) obj;
- for (final Object item : ar)
- if (item != null)
- root.addChild(introspect(item, null));
- } else {
- for (final Field field : getAllFields(type)) {
- if ((field.getModifiers() & Modifier.STATIC) != 0) {
- continue;
- }
- field.setAccessible(true);
- root.addChild(introspect(field.get(obj), field));
- }
- }
- }
-
- root.sort(); // sort by offset
- return root;
- }
-
- // get all fields for this class, including all superclasses fields
- private static List<Field> getAllFields(final Class type) {
- if (type.isPrimitive())
- return Collections.emptyList();
- Class cur = type;
- final List<Field> res = new ArrayList<Field>(10);
- while (true) {
- Collections.addAll(res, cur.getDeclaredFields());
- if (cur == Object.class)
- break;
- cur = cur.getSuperclass();
- }
- return res;
- }
-
- // check if it is an array of objects. I suspect there must be a more
- // API-friendly way to make this check.
- private static boolean isObjectArray(final Class type) {
- if (!type.isArray())
- return false;
- if (type == byte[].class || type == boolean[].class
- || type == char[].class || type == short[].class
- || type == int[].class || type == long[].class
- || type == float[].class || type == double[].class)
- return false;
- return true;
- }
-
- // advanced toString logic
- private static String getContents(final Object val, final Class type) {
- if (val == null)
- return "null";
- if (type.isArray()) {
- if (type == byte[].class)
- return Arrays.toString((byte[]) val);
- else if (type == boolean[].class)
- return Arrays.toString((boolean[]) val);
- else if (type == char[].class)
- return Arrays.toString((char[]) val);
- else if (type == short[].class)
- return Arrays.toString((short[]) val);
- else if (type == int[].class)
- return Arrays.toString((int[]) val);
- else if (type == long[].class)
- return Arrays.toString((long[]) val);
- else if (type == float[].class)
- return Arrays.toString((float[]) val);
- else if (type == double[].class)
- return Arrays.toString((double[]) val);
- else
- return Arrays.toString((Object[]) val);
- }
- return val.toString();
- }
-
- // obtain a shallow size of a field of given class (primitive or object
- // reference size)
- private static int getShallowSize(final Class type) {
- if (type.isPrimitive()) {
- final Integer res = primitiveSizes.get(type);
- return res != null ? res : 0;
- } else
- return objectRefSize;
- }
- }
先一個(gè)測試類來驗(yàn)證一下Unsafe的方式計(jì)算出的結(jié)果
- public class ClassIntrospectorTest
- {
- public static void main(String[] args) throws IllegalAccessException {
- final ClassIntrospector ci = new ClassIntrospector();
-
- ObjectInfo res;
-
- res = ci.introspect( new ObjectA() );
- System.out.println( res.getDeepSize() );
- }
-
- private static class ObjectA {
- String str; // 4
- int i1; // 4
- byte b1; // 1
- byte b2; // 1
- int i2; // 4
- ObjectB obj; //4
- byte b3; // 1
- }
-
- private static class ObjectB {
-
- }
- }
計(jì)算結(jié)果如下:
32
和我們之前計(jì)算結(jié)果是一致的,證明是正確的。
最后再來測試一下數(shù)組對(duì)象的長度。有兩個(gè)類如下:
- private static class ObjectC {
- ObjectD[] array = new ObjectD[2];
- }
-
- private static class ObjectD {
- int value;
- }
它們在內(nèi)存的大體分布如下圖:
我們可以手工計(jì)算一下ObjectC obj = new ObjectC()的大?。?/p>
ObjectC的Shallow size = 8(_mark) + 4(oop指針) + 4(ObjectD[]引用) = 16
new ObjectD[2]數(shù)組的長度 = 8(_mark) + 4(oop指針) + 4(數(shù)組長度占4個(gè)字節(jié)) + 4(ObjectD[0]引用) + 4(ObjectD[1]引用) = 24
由于ObjectD[]數(shù)組沒有指向具體的對(duì)象大小,所以我們手工計(jì)算的結(jié)果是16 + 24 = 40
使用Unsafe對(duì)象的方式來計(jì)算一下:
- public static void main(String[] args) throws IllegalAccessException {
- final ClassIntrospector ci = new ClassIntrospector();
-
- ObjectInfo res;
-
- res = ci.introspect( new ObjectC() );
- System.out.println( res.getDeepSize() );
- }
計(jì)算結(jié)果如下,和我們計(jì)算的結(jié)果是一致的,證明是正確的:
40
再給ObjectD[]數(shù)組指向具體的ObjectD對(duì)象,再測試一下結(jié)果:
- public static void main(String[] args) throws IllegalAccessException {
- final ClassIntrospector ci = new ClassIntrospector();
-
- ObjectInfo res;
-
- res = ci.introspect( new ObjectC() );
- System.out.println( res.getDeepSize() );
- }
-
- private static class ObjectC {
- ObjectD[] array = new ObjectD[2];
-
- public ObjectC(){
- array[0] = new ObjectD();
- array[1] = new ObjectD();
- }
- }
-
- private static class ObjectD {
- int value;
- }
我們可以手工計(jì)算一下ObjectC obj = new ObjectC()的大?。?p>
ObjectC的Shallow size = 8(_mark) + 4(oop指針) + 4(ObjectD[]引用) = 16
new ObjectD[2]數(shù)組的長度 = 8(_mark) + 4(oop指針) + 4(數(shù)組長度占4個(gè)字節(jié)) + 4(ObjectD[0]引用) + 4(ObjectD[1]引用) = 24
ObjectD對(duì)象長度 = 8(_mark) + 4(oop指針) + 4(value) = 16
所以O(shè)bjectC實(shí)際占用的空間 = 16 + 24 + 2 * 16 = 72
使用Unsafe的方式計(jì)算的結(jié)果也是72,和我們手工計(jì)算的方式一致。
參考: Memory introspection using sun.misc.Unsafe and reflection