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



        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位。




1. import
2.   
3. import
4.   
5. /**
6.  * -Xmx1024m
7.  * @author tianmai.fh
8.  * @date 2014-03-18 19:10
9.  */
10. public class
11. static
12.   
13. static
14. null;  
15. try
16. class.getDeclaredField("theUnsafe");  
17. true);  
18. null);  
19. catch
20.             e.printStackTrace();  
21. catch
22.             e.printStackTrace();  
23.         }  
24.     }  
25.   
26. static class
27. new
28. new Integer(3);  
29. int c = 4;  
30. long
31. new Long[2];  
32. new String[0];  
33.     }  
34. static class
35. int
36.         Integer b;  
37. int
38.     }  
39.   
40. static long
41. return
42.     }  
43.   
44. static
45.         Field[] fields = clazz.getDeclaredFields();  
46. new StringBuilder(fields.length * 50);  
47. " Field offset:\n");  
48. for
49. "\t").append(field.getType().getSimpleName());  
50. "\t").append(field.getName()).append(": ");  
51. "\n");  
52.         }  
53. return
54.     }  
55.   
56. public static void main(String[] args) throws
57. new
58. int[] big = new int[30 * 1024 * 1024];  
59. null;  
60.         System.gc();  
61. class)));  
62. class)));  
63. new Long[1];  
64. class.getName());  
65. 1000000);  
66.     }  
67. }


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


1. com.tmall.buy.structure.FieldOffsetTest$MyClass Field offset:  
2. 24
3. 28
4. int c: 12
5. long    d: 16
6. 32
7. 36
8.   
9. com.tmall.buy.structure.FieldOffsetTest$B2 Field offset:  
10. int a: 12
11. 20
12. int c: 16


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

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



1. com.tmall.buy.structure.FieldOffsetTest$MyClass Field offset:  
2. 32
3. 40
4. int c: 24
5. long    d: 16
6. 48
7. 56
8.   
9. com.tmall.buy.structure.FieldOffsetTest$B2 Field offset:  
10. int a: 16
11. 24
12. int c: 20


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

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



1. import
2. import
3. import
4. import
5.   
6. /**
7.  * 打印对象的内存布局
8.  */
9. public class PrintObjectTest extends
10. public static void main(String[] args) throws
11. new
12.         test.start(args);  
13.         test.stop();  
14.     }  
15.   
16. @Override
17. public void
18.         VM vm = VM.getVM();  
19.         ObjectHeap objHeap = vm.getObjectHeap();  
20. new
21. //观察特定对象
22. "xxx.yyy.zzz.FieldOffsetTest$MyClass");  
23. false);  
24.   
25. //观察数组对象
26. new
27. @Override
28. public boolean
29. return
30.             }  
31.         });  
32.         objHeap.iterate(heapVisitor);  
33.     }  
34. }


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



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就可以了:



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启动,会抛出如下错误:



1. Attaching to process ID 3024, please wait...  
2. Exception in thread "main"
3.     at sun.jvm.hotspot.debugger.bsd.BsdDebuggerLocal.init0(Native Method)  
4. 595)  
5. 775)  
6. 519)  
7. 492)  
8. 331)  
9. 163)  
10. 14)


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

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




    1. Oop for com/tmall/buy/structure/FieldOffsetTest$MyClass @ 0x000000011bfce258 (object size = 40)  
    2. 0} :1
    3. 8} :InstanceKlass for com/tmall/buy/structure/FieldOffsetTest$MyClass @ 0x0000000146d2a160
    4. 24} :Oop for java/lang/Object @ 0x000000011bf9bb90
    5. 28} :Oop for java/lang/Integer @ 0x000000011bf9bba8
    6. 12} :4
    7. 16} :5
    8. 32} :ObjArray @ 0x000000011bf9bbc0
    9. 36} :ObjArray @ 0x000000011bf9bbd8
    10.   
    11. ...  
    12.   
    13. ObjArray @ 0x000000011bf9bbc0 (object size = 24)  
    14. 0} :1
    15. 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. 0} :1
    23. 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字节对齐的。

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




    1. Oop for com/tmall/buy/structure/FieldOffsetTest$MyClass @ 0x000000011ad491e8 (object size = 64)  
    2. 0} :1
    3. 8} :InstanceKlass for com/tmall/buy/structure/FieldOffsetTest$MyClass @ 0x0000000145a873d8
    4. 32} :Oop for java/lang/Object @ 0x000000011ad1e1a8
    5. 40} :Oop for java/lang/Integer @ 0x000000011ad211b8
    6. 24} :4
    7. 16} :5
    8. 48} :ObjArray @ 0x000000011ad201c8
    9. 56} :ObjArray @ 0x000000011ad211d0
    10.   
    11. ...  
    12.   
    13. ObjArray @ 0x000000011ad201c8 (object size = 40)  
    14. 0} :1
    15. 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. 0} :1
    23. 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。