查看java对象在内存中的布局

     接着上篇《一个对象占用多少字节?》中遇到的问题:

        UseCompressOops开启和关闭,对对象头大小是有影响的,开启压缩,对象头是4+8=12byte;关闭压缩,对象头是8+8=16bytes。这个如何观察验证呢?
       基于上述事实,通过new A()和new B()占用字节推断,基本类型int在开启、关闭压缩情况下都是占用4个bytes的,这个没有影响。而通过B和B2在开启、关闭指针压缩情况下的对比看,Integer类型分别占了4 bytes和8 bytes,实际上引用类型都是这样。如何验证?
        new Integer[0]在压缩前后分别占用16、24个字节,这是又是为什么呢?

         其实要想验证这些信息,需要知道对象在内存中的布局,并且可以把他们输出出来,很巧看到了撒加(RednaxelaFX)大神的《借助HotSpot SA来一窥PermGen上的对象》,可以一窥java对象在内存中的布局。不过我没搞那么复杂,没用oom的方式输出内存对象信息——主要是由于在我的mac os x上Intellij IDEA权限的原因那样做不成功——而是通过启动两个进程的方式,一个监控程序和一个被监控程序。

        先写了个程序,也用unsafe的方法获取到字段偏移量,来跟通过SA的方式做对比。首先说明,我的os是Mac OSX 10.9.2,64bit机器,jdk是jdk1.7.0_11,64位。

Java代码   收藏代码
  1. import sun.misc.Unsafe;  
  2.   
  3. import java.lang.reflect.Field;  
  4.   
  5. /** 
  6.  * -Xmx1024m 
  7.  * @author tianmai.fh 
  8.  * @date 2014-03-18 19:10 
  9.  */  
  10. public class FieldOffsetTest {  
  11.     static Unsafe unsafe;  
  12.   
  13.     static {  
  14.         Field field = null;  
  15.         try {  
  16.             field = Unsafe.class.getDeclaredField("theUnsafe");  
  17.             field.setAccessible(true);  
  18.             unsafe = (Unsafe) field.get(null);  
  19.         } catch (NoSuchFieldException e) {  
  20.             e.printStackTrace();  
  21.         } catch (IllegalAccessException e) {  
  22.             e.printStackTrace();  
  23.         }  
  24.     }  
  25.   
  26.     static class MyClass {  
  27.         Object a = new Object();  
  28.         Integer b = new Integer(3);  
  29.         int c = 4;  
  30.         long d = 5L;  
  31.         Long[] e = new Long[2];  
  32.         Object[] f = new String[0];  
  33.     }  
  34.     static class B2 {  
  35.         int a;  
  36.         Integer b;  
  37.         int c;  
  38.     }  
  39.   
  40.     static long objectFieldOffset(Field field) {  
  41.         return unsafe.objectFieldOffset(field);  
  42.     }  
  43.   
  44.     static String objectFieldOffset(Class<?> clazz) {  
  45.         Field[] fields = clazz.getDeclaredFields();  
  46.         StringBuilder sb = new StringBuilder(fields.length * 50);  
  47.         sb.append(clazz.getName()).append(" Field offset:\n");  
  48.         for (Field field : fields) {  
  49.             sb.append("\t").append(field.getType().getSimpleName());  
  50.             sb.append("\t").append(field.getName()).append(": ");  
  51.             sb.append(objectFieldOffset(field)).append("\n");  
  52.         }  
  53.         return sb.toString();  
  54.     }  
  55.   
  56.     public static void main(String[] args) throws InterruptedException, NoSuchFieldException {  
  57.         MyClass mc = new MyClass();  
  58.         int[] big = new int[30 * 1024 * 1024];  
  59.         big = null;  
  60.         System.gc();  
  61.         System.out.println(objectFieldOffset((MyClass.class)));  
  62.         System.out.println(objectFieldOffset((B2.class)));  
  63.         Object a = new Long[1];  
  64.         System.out.println(Long[].class.getName());  
  65.         Thread.sleep(1000000);  
  66.     }  
  67. }  

         在启用指针压缩的情况下输出为:

Java代码   收藏代码
  1. com.tmall.buy.structure.FieldOffsetTest$MyClass Field offset:  
  2.     Object  a: 24  
  3.     Integer b: 28  
  4.     int c: 12  
  5.     long    d: 16  
  6.     Long[]  e: 32  
  7.     Object[]f: 36  
  8.   
  9. com.tmall.buy.structure.FieldOffsetTest$B2 Field offset:  
  10.     int a: 12  
  11.     Integer b: 20  
  12.     int c: 16  

        第一个实例变量的偏移量都是12,也就是说对象头占用了12个字节;基本类型int占用4个字节;对象引用占用了4个字节,如MyClass#a;对象数组占用也是4个字节;这里看不出数组这个对象占用了多少个字节。

        在不启用对象指针压缩的时候(vm参数添加-XX:-UseCompressedOops):

Java代码   收藏代码
  1. com.tmall.buy.structure.FieldOffsetTest$MyClass Field offset:  
  2.     Object  a: 32  
  3.     Integer b: 40  
  4.     int c: 24  
  5.     long    d: 16  
  6.     Long[]  e: 48  
  7.     Object[]    f: 56  
  8.   
  9. com.tmall.buy.structure.FieldOffsetTest$B2 Field offset:  
  10.     int a: 16  
  11.     Integer b: 24  
  12.     int c: 20  

       第一个实例变量的偏移量都是16,也就是说对象头占用了16个字节;基本类型int占用4个字节;对象引用占用了8个字节,如MyClass#a;对象数组占用也是8个字节;这里看不出数组这个对象占用了多少个字节。

       那接下来通过对象的内存布局进一步验证:

Java代码   收藏代码
  1. import sun.jvm.hotspot.oops.*;  
  2. import sun.jvm.hotspot.runtime.VM;  
  3. import sun.jvm.hotspot.tools.Tool;  
  4. import sun.jvm.hotspot.utilities.SystemDictionaryHelper;  
  5.   
  6. /** 
  7.  * 打印对象的内存布局 
  8.  */  
  9. public class PrintObjectTest extends Tool {  
  10.     public static void main(String[] args) throws InterruptedException {  
  11.         PrintObjectTest test = new PrintObjectTest();  
  12.         test.start(args);  
  13.         test.stop();  
  14.     }  
  15.   
  16.     @Override  
  17.     public void run() {  
  18.         VM vm = VM.getVM();  
  19.         ObjectHeap objHeap = vm.getObjectHeap();  
  20.         HeapVisitor heapVisitor = new HeapPrinter(System.out);  
  21.         //观察特定对象  
  22.         Klass klass = SystemDictionaryHelper.findInstanceKlass("xxx.yyy.zzz.FieldOffsetTest$MyClass");  
  23.         objHeap.iterateObjectsOfKlass(heapVisitor, klass, false);  
  24.   
  25.         //观察数组对象  
  26.         objHeap.iterate(heapVisitor,new ObjectHeap.ObjectFilter() {  
  27.             @Override  
  28.             public boolean canInclude(Oop oop) {  
  29.                 return oop.isObjArray();  
  30.             }  
  31.         });  
  32.         objHeap.iterate(heapVisitor);  
  33.     }  
  34. }  

        这个程序在运行前,需要传入要监控的java进程id,也就是上边那个程序的进程id,可以通过jps拿到。但是在我的IDEA上,是跑不起来的,是由于权限问题:

Java代码   收藏代码
  1. Attaching to process ID 1923, please wait...  
  2. attach: task_for_pid(1923) failed (5)  
  3. Error attaching to process: sun.jvm.hotspot.debugger.DebuggerException: Can't attach to the process  

        用命令行,sudo就可以了:

Java代码   收藏代码
  1. sudo java -cp $JAVA_HOME/lib/sa-jdi.jar:. xxx.yyy.zzz.PrintObjectTest 进程id > heap_OOps.txt  

        如果你被监控的jvm实例是1.7.x启动的,而命令行监控实例通过1.8的jdk启动,会抛出如下错误:

Java代码   收藏代码
  1. Attaching to process ID 3024, please wait...  
  2. Exception in thread "main" java.lang.NoSuchMethodError: getJavaThreadsInfo  
  3.     at sun.jvm.hotspot.debugger.bsd.BsdDebuggerLocal.init0(Native Method)  
  4.     at sun.jvm.hotspot.debugger.bsd.BsdDebuggerLocal.<clinit>(BsdDebuggerLocal.java:595)  
  5.     at sun.jvm.hotspot.bugspot.BugSpotAgent.setupDebuggerBsd(BugSpotAgent.java:775)  
  6.     at sun.jvm.hotspot.bugspot.BugSpotAgent.setupDebugger(BugSpotAgent.java:519)  
  7.     at sun.jvm.hotspot.bugspot.BugSpotAgent.go(BugSpotAgent.java:492)  
  8.     at sun.jvm.hotspot.bugspot.BugSpotAgent.attach(BugSpotAgent.java:331)  
  9.     at sun.jvm.hotspot.tools.Tool.start(Tool.java:163)  
  10.     at com.tmall.buy.structure.PrintObjectTest.main(PrintObjectTest.java:14)  

         直接全路径用1.7的jdk带的java启动就好了。

         接下来我们看输出,这个是启用指针压缩的,由于输出比较长,我们就只关心我们想看的几个:

Java代码   收藏代码
  1. Oop for com/tmall/buy/structure/FieldOffsetTest$MyClass @ 0x000000011bfce258 (object size = 40)  
  2.  - _mark:    {0} :1  
  3.  - _metadata._compressed_klass:  {8} :InstanceKlass for com/tmall/buy/structure/FieldOffsetTest$MyClass @ 0x0000000146d2a160  
  4.  - a:    {24} :Oop for java/lang/Object @ 0x000000011bf9bb90  
  5.  - b:    {28} :Oop for java/lang/Integer @ 0x000000011bf9bba8  
  6.  - c:    {12} :4  
  7.  - d:    {16} :5  
  8.  - e:    {32} :ObjArray @ 0x000000011bf9bbc0  
  9.  - f:    {36} :ObjArray @ 0x000000011bf9bbd8  
  10.   
  11. ...  
  12.   
  13. ObjArray @ 0x000000011bf9bbc0 (object size = 24)  
  14.  - _mark:    {0} :1  
  15.  - _metadata._compressed_klass:  {8} :ObjArrayKlass for InstanceKlass for java/lang/Long @ 0x0000000146d2b910  
  16.  - 0:    {16} :null  
  17.  - 1:    {20} :null  
  18.   
  19. ...  
  20.   
  21. ObjArray @ 0x000000011bf9bbd8 (object size = 16)  
  22.  - _mark:    {0} :1  
  23.  - _metadata._compressed_klass:  {8} :ObjArrayKlass for InstanceKlass for java/lang/String @ 0x0000000146b229c0  
  24.   
  25. ...  

         可以看到,MyClass这个类的大小是40个字节,不包括它引用的对象的大小,其中大括号是对象实例字段的偏移量,单位是字节。验证了对象头是12 bytes,其中_mark占8个字节_metadata._compressed_klass占用4个字节;剩下的就跟第一个例子中启用了压缩指针的结论一致。这里我们也可以看到数据对象占用的内存空间了,数组对象的头部占用了16个字节,_mark占8个,_metadata._compressed_klass占8个;另外也验证了,对象是8字节对齐的。

    在看不启用对象指针压缩的情况:

Java代码   收藏代码
  1. Oop for com/tmall/buy/structure/FieldOffsetTest$MyClass @ 0x000000011ad491e8 (object size = 64)  
  2.  - _mark:    {0} :1  
  3.  - _metadata._klass:     {8} :InstanceKlass for com/tmall/buy/structure/FieldOffsetTest$MyClass @ 0x0000000145a873d8  
  4.  - a:    {32} :Oop for java/lang/Object @ 0x000000011ad1e1a8  
  5.  - b:    {40} :Oop for java/lang/Integer @ 0x000000011ad211b8  
  6.  - c:    {24} :4  
  7.  - d:    {16} :5  
  8.  - e:    {48} :ObjArray @ 0x000000011ad201c8  
  9.  - f:    {56} :ObjArray @ 0x000000011ad211d0  
  10.   
  11. ...  
  12.   
  13. ObjArray @ 0x000000011ad201c8 (object size = 40)  
  14.  - _mark:    {0} :1  
  15.  - _metadata._klass:     {8} :ObjArrayKlass for InstanceKlass for java/lang/Long @ 0x0000000145a88120  
  16.  - 0:    {24} :null  
  17.  - 1:    {32} :null  
  18.   
  19. ...  
  20.   
  21. ObjArray @ 0x000000011ad211d0 (object size = 24)  
  22.  - _mark:    {0} :1  
  23.  - _metadata._klass:     {8} :ObjArrayKlass for InstanceKlass for java/lang/String @ 0x0000000145876ef0  
  24.   
  25. ...  

        MyClass这个类的大小是64个字节,不包括它引用的对象的大小,其中大括号是对象实例字段的偏移量,单位是字节。验证了对象头是16 bytes,其中_mark占8个字节_metadata._klass占用8个字节;剩下的就跟第一个例子中不启用了压缩指针的结论一致。数组对象的头部占用了24个字节,_mark占8个,_metadata._compressed_klass占16个;另外也验证了,对象是8字节对齐的。

        tips:在查找MyClass对象中数组类型实例字段的内存布局时,可以直接用后边的内存地址搜索@ 0x000000011ad201c8。

猜你喜欢

转载自jaesonchen.iteye.com/blog/2337674