在HotSpot虚拟机中,对象的内存布局可以分为三个部分:对象头、实例数据和对齐填充。对象头是Java对象在虚拟机中最神秘的部分,它不仅包含了指向对象类型数据的指针,还承载了极其关键的运行时元数据。其中,Mark Word作为对象头的核心区域,存储了对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志等。由于对象头的信息是与对象自身定义的数据无关的附加存储成本,虚拟机将其设计为一个非常紧凑的非固定数据结构,以便在极小的空间内记录尽可能多的状态信息。

在64位的HotSpot虚拟机中,Mark Word通常占用64个比特位,也就是8个字节。这64个比特位并不是固定不变的,而是会根据对象当前所处的状态动态改变其存储的内容。例如,当对象未被锁定时,Mark Word中主要存储的是对象的哈希码和GC分代年龄;而当对象被锁定后,这64个比特位中的部分区域会被用来存储指向锁记录的指针或指向重量级锁的指针。这种动态复用内存的设计,使得虚拟机无需为每个对象分配额外的锁状态内存,极大地节省了内存空间。
Java对象头的内存布局与Mark Word概述
要理解Mark Word的作用,首先需要了解Java对象在内存中的具体分布。在HotSpot虚拟机中,对象头包含两部分信息:第一部分是Mark Word,用于存储对象自身的运行时数据;第二部分是类型指针,即对象指向它的类元数据的内部指针,虚拟机通过这个指针来确定该对象是哪个类的实例。如果对象是一个Java数组,那么对象头中还会有一块用于记录数组长度的区域,因为虚拟机可以通过普通Java对象元数据确定大小,但数组对象的大小无法从元数据中获取。
Mark Word被设计为一个非固定的数据结构,目的是在极其有限的内存空间中存储尽量多的数据。它根据对象的状态变化,复用自身的存储空间。在32位虚拟机中,Mark Word为32位;在64位虚拟机中,Mark Word为64位。以64位为例,其内部的数据结构会根据对象是否被锁定、是否处于并发标记阶段等条件,呈现出完全不同的位段划分。这种设计是JVM实现高效并发控制和精准垃圾回收的基石。
Mark Word在锁升级过程中的标记位变化
Java为了提升并发性能,在JDK 6之后引入了锁升级机制。锁的状态主要分为无锁、偏向锁、轻量级锁和重量级锁四种。这些状态的变化直接反映在Mark Word的锁标志位上。在64位系统中,Mark Word的最后两位用于表示锁标志位,倒数第三位用于表示是否为偏向锁。无锁状态的标志位是01,偏向锁的标志位也是01但偏向位为1,轻量级锁的标志位是00,而重量级锁的标志位则是10。另外还有一个标志位11,表示对象被GC标记为不可达状态。
当线程第一次访问同步代码块时,虚拟机会将对象头设置为偏向锁状态。此时Mark Word中存储的是当前线程的ID。如果接下来没有其他线程竞争,持有偏向锁的线程在进入和退出同步块时甚至不需要进行CAS操作,这极大地提升了性能。我们可以使用JOL工具来直观地观察对象头的变化。通过引入jol-core依赖,可以在控制台打印出对象在内存中的具体布局,清晰地看到Mark Word的值随着锁的升级而改变。
import org.openjdk.jol.info.ClassLayout;
import java.util.concurrent.TimeUnit;
public class MarkWordDemo {
static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
// 打印无锁状态下的对象头
System.out.println("无锁状态:");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
// 开启偏向锁延迟,等待JVM开启偏向锁机制
TimeUnit.SECONDS.sleep(5);
synchronized (lock) {
// 打印偏向锁状态下的对象头
System.out.println("偏向锁状态:");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
}
}
当出现多个线程交替执行,但不存在激烈竞争时,偏向锁会升级为轻量级锁。此时,虚拟机会在当前线程的栈帧中建立一个锁记录空间,用于存储Mark Word的拷贝。对象头中的Mark Word会被更新为指向该锁记录的指针,锁标志位变为00。如果竞争更加激烈,导致自旋等待的线程超过一定阈值或等待时间过长,轻量级锁就会膨胀为重量级锁。此时Mark Word会被替换为指向操作系统互斥量的指针,锁标志位变为10,未获取到锁的线程将被挂起阻塞。
GC标记与锁标记位的复用与冲突
除了记录锁状态,Mark Word还承担着垃圾回收的重要职责。在Mark Word中,有一块区域专门用于存储GC分代年龄。分代年龄占用了4个比特位,这意味着它的最大值是15。这也是为什么Survivor区对象的最大年龄默认为15的原因。当对象经历一次Minor GC后依然存活,其分代年龄就会加1。当年龄达到阈值时,对象就会被晋升到老年代。
然而,在并发垃圾回收器(如CMS或G1)进行并发标记时,如何标记对象是否存活成为一个挑战。由于不能像Serial收集器那样在GC时暂停所有用户线程并直接修改对象头,并发收集器需要解决对象在标记过程中被用户线程修改引用关系的问题。为了实现并发标记,JVM巧妙地复用了Mark Word中的部分区域。在CMS收集器中,当对象被标记为存活时,会修改其Mark Word的最后两位为11,即进入标记状态。这种状态在GC完成后会被重置。
但是,这种复用机制带来了一个明显的问题:如果对象正处于锁状态,其Mark Word已经被用于存储锁指针,GC标记如何处理?实际上,当对象处于轻量级锁或重量级锁状态时,对象的Mark Word被指向了栈中的锁记录或堆中的ObjectMonitor对象。此时,GC无法直接修改对象头的Mark Word。为了解决这个问题,JVM在锁记录和ObjectMonitor中都预留了空间来保存对象原本的Mark Word。在GC标记阶段,垃圾收集器会通过这些指针找到对应的锁记录或监视器对象,并在那里进行标记操作,从而避免了锁标记与GC标记的直接冲突。