对象分配、布局、访问
探讨HotSpot虚拟机在java堆中对象分配、布局、访问的全过程
对象的创建
对象的创建总体而言分为以下五步:
1、类加载检查
当虚拟机遇到new指令时,先去检查这个指令的参数能否在常量池中定位到一个类的符号引用。
检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,执行相应的加载过程。
2、为新对象分配内存
分配内存主要是划分可用空间,另外需要考虑线程安全性。
划分空间
对象所需的大小在类加载完后可以完全确定。
根据垃圾收集器是否带有压缩整理功能,分配空间有两种方式:指针碰撞、空闲列表。
指针碰撞:堆中的内存是规整的,已用的放在一边,空闲的放在一边,中间用一个指针作为分界点的指示器,分配内存只是将指示器向空闲方移动指定大小的距离,这种方式叫做指针碰撞。
空闲列表:堆中的内存不是规整的,已用和空闲的空间交错。这时虚拟机就必须维护一个列表,记录哪些空间可用,分配时在列表上找一块足够大的空间划分给对象实例,同时更新列表上的记录,这种方式叫做空闲列表。
线程安全
由于堆空间是线程共享的,对象的分配又是一个非常频繁的行为,因此必须考虑线程安全。
同步:对分配内存空间的动作进行同步处理,实际虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。
TLAB:每个线程在堆中预先分配一小块内存,称为TLAB。哪个线程需要分配内存,就在哪个TLAB上分配。只有TLAB用完并分配新的TLAB时,才需要同步锁定。虚拟机是否用TLAB,可以用-XX:+/-UseTLAB参数来设定。
3、内存初始化为零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),如果使用TLAB,这一动作会提前至TLAB分配时执行。
4、对对象进行必要设置
设置对象头。例如:对象是哪个类的实例,如何找到类的元数据信息、对象的哈希码等。
5、执行init方法
按照代码进行初始化。
对象的内存布局
对象在内存中的布局可以分为3块区域:对象头、实例数据、对齐补充。
1、对象头
对象头包括两部分信息,一是用于存储对象自身的运行时数据;二是类型指针,虚拟机用这个指针确定这个对象是哪个类的实例。特别的是,如果是一个java数组,对象头中还需要记录数组的长度,这样才能确定java对象的大小。
2、实例数据
实例数据是对象存储的真正有效信息。
3、对齐补充
对象的大小必须是8的整数倍,当对象的实例数据没有对齐时,通过对齐填充来补全。
对象的访问定位
java对象通过栈上的引用来操作堆上的具体对象。这个引用如何来操作堆上的对象呢?有句柄和直接指针两种方式。
句柄
java堆中划分出一块内存用来作为句柄池,引用指向句柄地址,句柄中存储的是对象的实际地址。
用句柄的好处:对象被移动时(垃圾回收时移动对象是比较普遍的行为)只会改变句柄中的实例数据指针,引用本身不需要修改。
直接指针
直接指针顾名思义,引用指向了具体对象的地址。
直接指针的好处:直接指针节省了一次指针定位的开销,由于对象的访问非常频繁,因此可以节约很多的执行成本。HotSpot采用直接指针。