对象的内存布局
对象在内存中存储的布局可以分为三部分:对象头、实例数据、对齐填充。
- 对象头
对象头包括两块信息。
对象头第一部分存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。这些数据官方称谓为“Mark Word”。这部分数据受虚拟机位数的影响,在32位虚拟机中大小是32bit,在64位虚拟机中大小是64bit。
对象头的另一部分是类型指针,即对象指向它类元数据的指针。虚拟机通过这个指针来确定这个对象是哪个类的实例。如果对象是一个数组,还需要对象头中有一小块内存用于记录数组长度,因为虚拟机可以通过普通java对象的元数据确定对象的大小,而对于数组,虚拟机无法从数组元数据中确定数组的大小。 - 实例数据
实例数据部分是对象真正存储的有效信息,也就是程序代码中所定义的各种类型的字段内容,无论是从父类继承还是子类中定义的都需要记录下来。
这部分的存储顺序会受到虚拟机分配策略参数和字段在Java源码中定义顺序的影响,相同长度的字段一般会被分到一起。在上述前提下,父类定义的变量一般会出现在子类变量之前。(仅限于部分类型虚拟机的大部分情况) - 对齐填充
对齐填充这部分不是必然存在的,也没有啥意义,它就是起占位符的作用。很多虚拟机要求对象的大小必须是8的倍数,对象头部分是规定的8的倍数,所以当实例数据部分没有对齐时,就需要对齐填充来补齐。
对象的访问定位
由于对象的实例存放在堆中,java虚拟机栈中只记录了对象引用(reference),并没有在这个引用中定义如何定义并访问java堆中对象的具体位置,所以对象的访问定位是由java虚拟机决定的,当前常用的两种方式是使用句柄和直接指针两种。
- 使用句柄
由于新创建的对象数据分为两部分:对象实例数据和对象类型数据,其中对象实例数据存储在java堆中,对象类型数据存储在方法区,所以在使用句柄的定位方式中,java堆中会分配出一小块儿区域用于存放对象实例数据和对象类型数据的具体存放地址信息,称为句柄池。而java虚拟机栈中的reference就是指向该句柄池。
使用句柄的定位方式在java虚拟机栈的reference中存储了稳定的句柄地址,在对象被移动时只需要修改句柄中的地址即可,reference中的数据信息不需要修改。 - 直接指针
如果把虚拟机栈中的reference直接指向java堆中的对象实例数据,并在对象实例数据中用一个指针记录方法区中对象类型数据的位置信息,那么就变成了直接指针的方式。
直接指针的最大优点就是速度快,因为直接指针比使用句柄少了一层指针,节约了一次指针定位的时间。