Java堆中对象的分配、布局和访问(HotSpot虚拟机)

130 阅读4分钟

「深入理解Java虚拟机」第2.3节读书笔记。

2.3.1 对象的创建

(讨论普通Java对象,不包括数组和Class对象)

虚拟机遇到一条new指令时,

(1)类加载检查:检查指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析、初始化过。如果没有,先执行相应的类加载过程。

(2)分配内存空间:把一块确定大小的内存从Java堆中划分出来(对象所需的内存大小在类加载完成后便可完全确定)

分配方式(两种): 

①垃圾收集器带有压缩整理功能(复制算法、标记-整理算法) -> Java堆中内存绝对规整 -> **“指针碰撞”分配法**(用过的内存和空闲的内存之间分界的指针向空闲空间那边挪动一段与对象大小相等的距离) 

②垃圾收集器没有压缩整理功能(标记-清除算法) -> Java堆中内存不是规整的 -> **“空闲列表”分配法**(虚拟机维护一个列表记录哪些内存块是可用的)

(3)初始化零值:内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

(4)设置对象头:例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄、是否启用偏向锁等信息。

(5)执行 init 方法:在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始。一般来说,执行 new 指令之后会接着执行 <init>方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

创建对象如何保证线程安全?(两种方法)

①对分配内存空间的动作进行同步处理。 CAS+失败重试: CAS是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。

②本地线程分配缓存TLAB: 为每一个线程预先在Eden区分配一块儿内存,JVM在给线程中的对象分配内存时,首先在TLAB分配,当对象大于TLAB中的剩余内存或TLAB的内存已用尽时,再采用上述的CAS进行内存分配(只有TLAB用完并分配新的TLAB时才需要同步锁定)

2.3.2 对象的内存布局

  1. 列表项对象头(与对象自身定义的数据无关的额外存储成本)

    • 对象自身的运行时数据(哈希吗、GC分代年龄、锁状态标志等)
    • 类型指针(虚拟机通过这个指针来确定这个对象是哪个类的实例)
    • 记录数组长度的数据(对象是一个Java数组时)
  2. 实例数据(对象真正存储的有效信息) 包括从父类继承下来的和在子类中定义的,相同宽度的字段被分配到一起,满足这个前提条件的情况下,父类中定义的变量出现在子类之前。

  3. 对齐填充(占位符,用于补全) 对象的大小必须是8字节的整数倍。对象头部分正好是8字节的倍数(1倍或2倍)(32位的机器上是64位 8字节,64位的机器上是128位 16字节),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

2.3.3 对象的访问定位

  1. 句柄访问 Java堆中划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与对象类型数据各自的具体地址。 优点:对象被移动时只会改变句柄中的实例数据指针,reference本身不需要修改
  2. 直接指针访问 reference 中存储的直接就是对象的地址。 优点:速度更快,节省了一次指针定位的时间开销