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

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

import sun.misc.Unsafe;

import java.lang.reflect.Field;

/**
 * -Xmx1024m
 * @author tianmai.fh
 * @date 2014-03-18 19:10
 */
public class FieldOffsetTest {
    static Unsafe unsafe;

    static {
        Field field = null;
        try {
            field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    static class MyClass {
        Object a = new Object();
        Integer b = new Integer(3);
        int c = 4;
        long d = 5L;
        Long[] e = new Long[2];
        Object[] f = new String[0];
    }
    static class B2 {
        int a;
        Integer b;
        int c;
    }

    static long objectFieldOffset(Field field) {
        return unsafe.objectFieldOffset(field);
    }

    static String objectFieldOffset(Class<?> clazz) {
        Field[] fields = clazz.getDeclaredFields();
        StringBuilder sb = new StringBuilder(fields.length * 50);
        sb.append(clazz.getName()).append(" Field offset:\n");
        for (Field field : fields) {
            sb.append("\t").append(field.getType().getSimpleName());
            sb.append("\t").append(field.getName()).append(": ");
            sb.append(objectFieldOffset(field)).append("\n");
        }
        return sb.toString();
    }

    public static void main(String[] args) throws InterruptedException, NoSuchFieldException {
        MyClass mc = new MyClass();
        int[] big = new int[30 * 1024 * 1024];
        big = null;
        System.gc();
        System.out.println(objectFieldOffset((MyClass.class)));
        System.out.println(objectFieldOffset((B2.class)));
        Object a = new Long[1];
        System.out.println(Long[].class.getName());
        Thread.sleep(1000000);
    }
}

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

 

com.tmall.buy.structure.FieldOffsetTest$MyClass Field offset:
	Object	a: 24
	Integer	b: 28
	int	c: 12
	long	d: 16
	Long[]	e: 32
	Object[]f: 36

com.tmall.buy.structure.FieldOffsetTest$B2 Field offset:
	int	a: 12
	Integer	b: 20
	int	c: 16

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

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

com.tmall.buy.structure.FieldOffsetTest$MyClass Field offset:
	Object	a: 32
	Integer	b: 40
	int	c: 24
	long	d: 16
	Long[]	e: 48
	Object[]	f: 56

com.tmall.buy.structure.FieldOffsetTest$B2 Field offset:
	int	a: 16
	Integer	b: 24
	int	c: 20

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

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

import sun.jvm.hotspot.oops.*;
import sun.jvm.hotspot.runtime.VM;
import sun.jvm.hotspot.tools.Tool;
import sun.jvm.hotspot.utilities.SystemDictionaryHelper;

/**
 * 打印对象的内存布局
 */
public class PrintObjectTest extends Tool {
    public static void main(String[] args) throws InterruptedException {
        PrintObjectTest test = new PrintObjectTest();
        test.start(args);
        test.stop();
    }

    @Override
    public void run() {
        VM vm = VM.getVM();
        ObjectHeap objHeap = vm.getObjectHeap();
        HeapVisitor heapVisitor = new HeapPrinter(System.out);
        //观察特定对象
        Klass klass = SystemDictionaryHelper.findInstanceKlass("xxx.yyy.zzz.FieldOffsetTest$MyClass");
        objHeap.iterateObjectsOfKlass(heapVisitor, klass, false);

        //观察数组对象
        objHeap.iterate(heapVisitor,new ObjectHeap.ObjectFilter() {
            @Override
            public boolean canInclude(Oop oop) {
                return oop.isObjArray();
            }
        });
        objHeap.iterate(heapVisitor);
    }
}

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

Attaching to process ID 1923, please wait...
attach: task_for_pid(1923) failed (5)
Error attaching to process: sun.jvm.hotspot.debugger.DebuggerException: Can't attach to the process

        用命令行,sudo就可以了:

sudo java -cp $JAVA_HOME/lib/sa-jdi.jar:. xxx.yyy.zzz.PrintObjectTest 进程id > heap_OOps.txt

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

Attaching to process ID 3024, please wait...
Exception in thread "main" java.lang.NoSuchMethodError: getJavaThreadsInfo
	at sun.jvm.hotspot.debugger.bsd.BsdDebuggerLocal.init0(Native Method)
	at sun.jvm.hotspot.debugger.bsd.BsdDebuggerLocal.<clinit>(BsdDebuggerLocal.java:595)
	at sun.jvm.hotspot.bugspot.BugSpotAgent.setupDebuggerBsd(BugSpotAgent.java:775)
	at sun.jvm.hotspot.bugspot.BugSpotAgent.setupDebugger(BugSpotAgent.java:519)
	at sun.jvm.hotspot.bugspot.BugSpotAgent.go(BugSpotAgent.java:492)
	at sun.jvm.hotspot.bugspot.BugSpotAgent.attach(BugSpotAgent.java:331)
	at sun.jvm.hotspot.tools.Tool.start(Tool.java:163)
	at com.tmall.buy.structure.PrintObjectTest.main(PrintObjectTest.java:14)

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

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

Oop for com/tmall/buy/structure/FieldOffsetTest$MyClass @ 0x000000011bfce258 (object size = 40)
 - _mark:	 {0} :1
 - _metadata._compressed_klass:	 {8} :InstanceKlass for com/tmall/buy/structure/FieldOffsetTest$MyClass @ 0x0000000146d2a160
 - a:	 {24} :Oop for java/lang/Object @ 0x000000011bf9bb90
 - b:	 {28} :Oop for java/lang/Integer @ 0x000000011bf9bba8
 - c:	 {12} :4
 - d:	 {16} :5
 - e:	 {32} :ObjArray @ 0x000000011bf9bbc0
 - f:	 {36} :ObjArray @ 0x000000011bf9bbd8

...

ObjArray @ 0x000000011bf9bbc0 (object size = 24)
 - _mark:	 {0} :1
 - _metadata._compressed_klass:	 {8} :ObjArrayKlass for InstanceKlass for java/lang/Long @ 0x0000000146d2b910
 - 0:	 {16} :null
 - 1:	 {20} :null

...

ObjArray @ 0x000000011bf9bbd8 (object size = 16)
 - _mark:	 {0} :1
 - _metadata._compressed_klass:	 {8} :ObjArrayKlass for InstanceKlass for java/lang/String @ 0x0000000146b229c0

...

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

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

Oop for com/tmall/buy/structure/FieldOffsetTest$MyClass @ 0x000000011ad491e8 (object size = 64)
 - _mark:	 {0} :1
 - _metadata._klass:	 {8} :InstanceKlass for com/tmall/buy/structure/FieldOffsetTest$MyClass @ 0x0000000145a873d8
 - a:	 {32} :Oop for java/lang/Object @ 0x000000011ad1e1a8
 - b:	 {40} :Oop for java/lang/Integer @ 0x000000011ad211b8
 - c:	 {24} :4
 - d:	 {16} :5
 - e:	 {48} :ObjArray @ 0x000000011ad201c8
 - f:	 {56} :ObjArray @ 0x000000011ad211d0

...

ObjArray @ 0x000000011ad201c8 (object size = 40)
 - _mark:	 {0} :1
 - _metadata._klass:	 {8} :ObjArrayKlass for InstanceKlass for java/lang/Long @ 0x0000000145a88120
 - 0:	 {24} :null
 - 1:	 {32} :null

...

ObjArray @ 0x000000011ad211d0 (object size = 24)
 - _mark:	 {0} :1
 - _metadata._klass:	 {8} :ObjArrayKlass for InstanceKlass for java/lang/String @ 0x0000000145876ef0

...

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

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

        关于对象更多描述信息,请移步到开篇引用的RednaxelaFX那篇文章中学习。enjoy it!