设置对象头过程介绍

在JVM中,当通过 new 指令为一个对象成功分配内存并完成零值初始化之后,接下来的一个重要步骤就是设置对象头(Object Header)。对象头是存放在对象实例内存区域起始位置的一块元数据,它不包含对象的实例数据(即字段值),而是存储了对象自身的一些状态信息和指向其类型信息的指针。

对象头的结构和内容会根据JVM的实现、对象的类型(是否为数组)、以及对象所处的状态(如锁状态)而有所不同。以HotSpot虚拟机为例,其对象头通常包含以下两部分或三部分信息(取决于是否为数组对象):

  1. Mark Word(标记字):

    • 这部分数据用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄(Generational GC Age)、锁状态标志、线程持有的锁(偏向锁的线程ID、轻量级锁指针)、重量级锁指针(指向monitor对象的指针)等。
    • Mark Word的设计是动态的,它会根据对象的状态复用自己的存储空间来存储不同的信息。例如:
      • 无锁状态:存储对象的哈希码、GC分代年龄、是否偏向锁(0或1)等。
      • 偏向锁状态:存储持有偏向锁的线程ID、Epoch、GC分代年龄、偏向锁标志位(1)。
      • 轻量级锁状态:存储指向线程栈中锁记录(Lock Record)的指针、锁标志位(00)。
      • 重量级锁状态:存储指向互斥量(Monitor)的指针、锁标志位(10)。
      • GC标记状态:在GC期间,Mark Word也可能被用于存储GC相关信息。
    • Mark Word的位数在32位JVM中是32位(4字节),在64位JVM中是64位(8字节)。
  2. Klass Pointer(类型指针,或称元数据指针):

    • 这部分数据是一个指针,指向该对象所属的类在方法区(或元空间)中的元数据(Klass对象)。虚拟机通过这个指针来确定这个对象是哪个类的实例,从而能够访问到类的字段信息、方法信息、父类信息等。
    • 如果开启了压缩类指针(Compressed Class Pointers,在64位JVM中通常默认开启,通过 -XX:+UseCompressedClassPointers),这个指针的大小会被压缩到32位(4字节)。如果未开启或无法压缩,它在64位JVM中是64位(8字节)。
  3. Array Length(数组长度,仅数组对象包含):

    • 如果当前创建的对象是一个数组对象,那么对象头中还必须有一块数据用于记录数组的长度。
    • 这部分数据的位数通常是32位(4字节),即使在64位JVM中,因为Java数组的最大长度受限于 int 类型的最大值。

设置对象头的过程具体发生在内存分配和零值初始化之后,执行<init>方法之前。JVM会根据以下信息填充对象头:

  • 对于Mark Word:

    • 初始状态通常是无锁状态,可能会设置初始的GC分代年龄(通常为0)。
    • 哈希码通常是延迟计算的,即在对象第一次调用 hashCode() 方法时才计算并存入Mark Word,或者在需要时(如对象要被放入哈希表)计算。
    • 锁相关的标志位会根据后续的同步操作动态变化。
  • 对于Klass Pointer:

    • JVM在执行 new 指令时,已经知道了要创建的是哪个类的对象。在类加载完成后,该类在方法区/元空间的 Klass 对象地址是确定的。因此,JVM会将这个地址(或压缩后的地址)写入对象头的Klass Pointer部分。
  • 对于Array Length(如果是数组对象):

    • 在创建数组对象时(例如 new int[10]),数组的长度(这里是10)是已知的。JVM会将这个长度值写入对象头的数组长度部分。

这个设置过程是由JVM自动完成的,对Java程序员是透明的。一旦对象头设置完毕,再加上后续<init>方法的执行(初始化实例字段),一个完整的、可用的对象就创建成功了。

总结一下设置对象头的过程: 1. 确定对象头的结构(是否为数组,决定是否有数组长度部分)。 2. 初始化Mark Word:设置初始的GC分代年龄、锁状态标志(通常为无锁),为哈希码预留空间或设置初始值。 3. 设置Klass Pointer:将指向方法区/元空间中对应类元数据的指针写入。 4. (如果为数组对象)设置Array Length:将数组的长度值写入。

这个过程确保了对象在创建后就拥有了必要的运行时元信息,为后续的垃圾回收、同步操作、类型判断等JVM行为提供了基础。