Memory introspection using sun.misc.Unsafe and reflection

by Mikhail Vorontsov

It is useful for a serious Java developer to realize how much memory is occupied by one or another Java object. You may have heard that we live in a world there memory is not an issue anymore. This may be true for your text editor ( though, try to open a large document with tons of embedded images and charts and see how much memory will be consumed by your favourite editor ). This may be true for a dedicated server software (at least, until your business would grow to a bigger market or you will run another piece of software on the same server). This is may even be true for a cloud-based software, if you are rich enough in order to pay a premium for a top-class server hardware.

Still, in the real world your software will once reach a point where it makes sense to spend money in its optimization rather than trying to obtain an even better hardware (currently the most you can get on a commodity class server is 64G RAM). At this point you will have to analyze your application and find which data structures consume most of application memory. The best tool for such task is a good profiler, but you can start with a cheaper approach of analyzing your objects inside your code. This article describes an Oracle JDK based ClassIntrospector class, which will allow you to analyze your application memory consumption.

I have already mentioned the Java object memory layout in the String packing part 1: converting characters to bytes article. For example, I have written that a 28 character long String should occupy 104 bytes before Java 1.7.0_06. To be honest, at the time of writing of that article I used my profiler to get a proof for my calculations. Now it is about time to implement a Java object introspector using pure Java and Oracle JDK specific sun.misc.Unsafe class.

We will use the following sun.misc.Unsafe methods:

1
2
3
4
5
6
7
8
9
//get offset of a non-static field in the object in bytes
public native long objectFieldOffset(java.lang.reflect.Field field);
//get offset of a first element in the array
public native int arrayBaseOffset(java.lang.Class aClass);
//get size of an element in the array
public native int arrayIndexScale(java.lang.Class aClass);
//get address size for your JVM
public native int addressSize();
    
//get offset of a non-static field in the object in bytes
public native long objectFieldOffset(java.lang.reflect.Field field);
//get offset of a first element in the array
public native int arrayBaseOffset(java.lang.Class aClass);
//get size of an element in the array
public native int arrayIndexScale(java.lang.Class aClass);
//get address size for your JVM
public native int addressSize();
    

There are 2 more introspection-related methods in sun.misc.Unsafe, which we will not use in this article: staticFieldBase and staticFieldOffset. These methods could be useful for unsafe read-write access to static fields.

How shall we find a memory layout of an object?

  1. Obtain all object fields, including its parent class fields, recursively calling Class.getDeclaredFields on the class and its superclasses.
  2. For all non-static fields (Field.getModifiers() & Modifiers.STATIC) obtain an offset of a field in its parent object using Unsafe.objectFieldOffset and a shallow field size: predefined values for primitives and either 4 or 8 bytes for object references (read more below).
  3. For arrays, call Unsafe.arrayBaseOffset and Unsafe.arrayIndexScale. The total shallow size of an array would be offset + scale * Array.getLength(array) and, of course, a reference to the array itself (see previous point).
  4. Do not forget that there may be a circular references in the object graph, so you will need to track all previously visited objects (IdentityHashMap is recommended for such cases).

Java Object reference size is quite a virtual value. It may be equal to 4 or 8 bytes depending on your JVM settings and on the amount of memory you have given to your JVM. It is always 8 bytes for heaps over 32G, but for smaller heaps it is 4 bytes unless you will turn off -XX:-UseCompressedOops JVM setting (I’m not sure in what JVM release this feature was added or turned on by default). As a result, the safest way to obtain an Object reference size is to find a size of an element in Object[] array: unsafe.arrayIndexScale( Object[].class ). Unsafe.addressSize proved to be not so useful for this purpose.

A small implementation note on 4 byte references on under 32G heaps. A normal 4 byte pointer could address any byte in 4G address space. If we will assume that all allocated objects will be aligned by 8 bytes boundary, we won’t need 3 lowest bits in our 32 bit pointers anymore (these bits will always be equal to zeroes). This means that we can store 35 bit addresses in 32 bit value:

32_bit_reference = ( int ) ( actual_64_bit_pointer >> 3 )

35 bits allow you to address 32 bit * 8 = 4G * 8 = 32G address space.

What other interesting things I have found while working on this tool?

  • You have to use Arrays.toString in order to print contents of all sorts of arrays (including both primitive and object arrays).
  • You have to be careful with boxing – introspection method accepts only Object-s as field values, so you may end up in the infinite loop: int is packed into Integer in order to be passed into the introspection method. Inside it you will find an Integer.value field and try to introspect it again – voila, you are back where you started from!
  • Do not forget to introspect all non-null values in any Object arrays – this is just an extra level of indirection in the object graph.

How to use ClassIntrospector class? Just instantiate it and call its instance introspect method on any of your objects. It will return you an ObjectInfo object, which relates to your “root” object. This object will point to all its children. I think it may be sufficient to print its toString method result and/or to call ObjectInfo.getDeepSize method, which will return you a total memory consumption of all objects referenced via your “root” object.

ClassIntrospector is not thread safe, but you may call introspect method any number of times from the same thread.

Summary

  • You can use the following sun.misc.Unsafe methods for obtaining Java object layout information: objectFieldOffset, arrayBaseOffset and arrayIndexScale.
  • Java Object reference size depends on your environment. It may be equal to 4 or 8 bytes depending on your JVM settings and on the amount of memory you have given to your JVM. It is always 8 bytes for heaps over 32G, but for smaller heaps it is 4 bytes unless you will turn off -XX:-UseCompressedOops JVM setting.

Source code

ClassIntrospector:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
import sun.misc.Unsafe;
 
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.util.*;
 
/**
 * This class could be used for any object contents/memory layout printing.
 */
public class ClassIntrospector
{
    public static void main(String[] args) throws IllegalAccessException {
        final ClassIntrospector ci = new ClassIntrospector();
        final Map&ltString, BigDecimal> map = new HashMap<String, BigDecimal>( 10);
        map.put( "one", BigDecimal.ONE );
        map.put( "zero", BigDecimal.ZERO );
        map.put( "ten", BigDecimal.TEN );
        final ObjectInfo res;
        res = ci.introspect( "0123456789012345678901234567" );
        //res = ci.introspect( new TestObjChild() );
        //res = ci.introspect(map);
        //res = ci.introspect( new String[] { "str1", "str2" } );
        //res = ci.introspect(ObjectInfo.class);
        //res = ci.introspect( new TestObj() );
 
        System.out.println( res.getDeepSize() );
        System.out.println( res );
    }
 
    /** First test object - testing various arrays and complex objects */
    private static class TestObj
    {
        protected final String[] strings = { "str1", "str2" };
        protected final int[] ints = { 14, 16 };
        private final Integer i = 28;
        protected final BigDecimal bigDecimal = BigDecimal.ONE;
 
        @Override
        public String toString() {
            return "TestObj{" +
                    "strings=" + (strings == null ? null : Arrays.asList(strings)) +
                    ", ints=" + Arrays.toString( ints ) +
                    ", i=" + i +
                    ", bigDecimal=" + bigDecimal +
                    '}';
        }
    }
 
    /** Test class 2 - testing inheritance */
    private static class TestObjChild extends TestObj
    {
        private final boolean[] flags = { true, true, false };
        private final boolean flag = false;
 
        @Override
        public String toString() {
            return "TestObjChild{" +
                    "flags=" + Arrays.toString( flags ) +
                    ", flag=" + flag +
                    '}';
        }
    }
 
    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;
    }
}
import sun.misc.Unsafe;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.util.*;

/**
 * This class could be used for any object contents/memory layout printing.
 */
public class ClassIntrospector
{
    public static void main(String[] args) throws IllegalAccessException {
        final ClassIntrospector ci = new ClassIntrospector();
        final Map&ltString, BigDecimal> map = new HashMap<String, BigDecimal>( 10);
        map.put( "one", BigDecimal.ONE );
        map.put( "zero", BigDecimal.ZERO );
        map.put( "ten", BigDecimal.TEN );
        final ObjectInfo res;
        res = ci.introspect( "0123456789012345678901234567" );
        //res = ci.introspect( new TestObjChild() );
        //res = ci.introspect(map);
        //res = ci.introspect( new String[] { "str1", "str2" } );
        //res = ci.introspect(ObjectInfo.class);
        //res = ci.introspect( new TestObj() );

        System.out.println( res.getDeepSize() );
        System.out.println( res );
    }

    /** First test object - testing various arrays and complex objects */
    private static class TestObj
    {
        protected final String[] strings = { "str1", "str2" };
        protected final int[] ints = { 14, 16 };
        private final Integer i = 28;
        protected final BigDecimal bigDecimal = BigDecimal.ONE;

        @Override
        public String toString() {
            return "TestObj{" +
                    "strings=" + (strings == null ? null : Arrays.asList(strings)) +
                    ", ints=" + Arrays.toString( ints ) +
                    ", i=" + i +
                    ", bigDecimal=" + bigDecimal +
                    '}';
        }
    }

    /** Test class 2 - testing inheritance */
    private static class TestObjChild extends TestObj
    {
        private final boolean[] flags = { true, true, false };
        private final boolean flag = false;

        @Override
        public String toString() {
            return "TestObjChild{" +
                    "flags=" + Arrays.toString( flags ) +
                    ", flag=" + flag +
                    '}';
        }
    }

    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;
    }
}

ObjectInfo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
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 );
    }
 
    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() )
            size += children.get( children.size() - 1 ).offset + children.get( children.size() - 1 ).length;
        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( &#039;\t&#039; );
        return sb;
    }
}
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 );
    }

    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() )
            size += children.get( children.size() - 1 ).offset + children.get( children.size() - 1 ).length;
        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( &#039;\t&#039; );
        return sb;
    }
}

3 thoughts on “Memory introspection using sun.misc.Unsafe and reflection

  1. Pingback: Book review: Java Performance: The Definitive Guide by Scott Oaks   | Java Performance Tuning Guide

  2. Pingback: Going over Xmx32G heap boundary means you will have less memory available  - Java Performance Tuning Guide

  3. Pingback: An overview of memory saving techniques in Java  - Java Performance Tuning Guide

Leave a Reply

Your email address will not be published. Required fields are marked *