探讨HotSpot虚拟机在Java堆中的对象分配、布局和访问过程。
对象的创建
这里仅讨论普通的Java对象,不涉及数组和Class对象等
对象的创建过程:
- 虚拟机遇到一条
new
指令时,首先去检查这个指令的参数是否能够在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化过。 类加载检查通过后,接着虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后可知。
Java堆中的内存分配有两种方式:
指针碰撞(Bump the Pointer):假设Java堆中内存是绝对规整的,所有用过的内存都放在一遍,没用过的内存放在另一边, 中间放着一个指针作为分界点的指示器。通过挪动指针来分配所需内存。
空闲列表(Free List):对于Java堆中的内存并不是规整的,虚拟机维护一个列表,记录上哪些内存块是可用的,分配的时候从列表中找到一块足够大的空间。分配方式是由Java堆是否规整决定,而Java堆是否规整是由所采用的垃圾回收器是否带有压缩整理的功能决定。
内存分配完成后,虚拟机将分配的内存空间都初始化为零值。
- 接着,虚拟机要对对象进行必要的设置。例如这个对象是哪个类的实例,如何才能找到类的元数据信息,对象的哈希码,对象的GC分代年龄等信息。这些信息存放在对象头里(Header)。
上述创建过程完成后,从虚拟机角度来看,一个新的对象以及产生,但从Java开发者的视角来看,对象创建才刚刚开始(实例构造器
<init>()
方法还没执行)。
对象的内存布局
在HotSpot虚拟机中,对象在内存中存储布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
对象头
HotSpot虚拟机的对象头包括两部分信息:
- 用于存储对象自身的运行时数据
例如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。 - 类型指针
类型指针:对象指向它的类元数据的指针,虚拟机通过这个指针确定这个对象是哪个类的实例。
实例数据
实例数据部分是对象真正存储的有效信息,也是代码中定义的各字段的内容。
无论是从父类继承下来的,还是在子类中定义的,都要记录起来。
对齐填充
对齐填充并不是必须的,占位符的作用。由于HotSpot虚拟机的自动内存管理要求对象的大小必须是8字节的整数倍。
对象的访问定位
Java程序需要通过栈上的reference数据来操作堆上的具体对象,reference类型在Java虚拟机规范中只规定了一个对象的引用,并没有定义这个引用通过何种方式去定位、访问堆中的具体位置。
对象的访问方式取决于虚拟机实现,目前主流的访问方式有使用句柄和直接指针两种。
句柄访问
如果使用句柄访问的话,那么Java堆中会划分出一块内存作为句柄池,reference存储的就是对象的句柄地址。句柄中包含了对象的实例数据和类型数据的具体地址信息。
直接指针访问
如果使用直接指针访问,那么Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址。
两者比较
使用句柄访问最大的好处是存储的是稳定句柄地址,对象被移动(例如垃圾回收移动对象)时只会改变句柄中的实例数据指针,而reference本身不需要修改。
使用直接指针访问最大的好处是速度更快,节省了一次指针定位的时间开销。
HotSpot中使用的是直接指针进行对象访问的。