研究 synchronized 的底层实现时,几乎所有人都会在同一个地方卡住:对象已经膨胀为重量级锁,Mark Word 里那串指向 ObjectMonitor 的指针到底是怎么来的,又是怎么被读取的。要彻底搞懂这个问题,不能停留在“锁标志位是 10”这种结论层面,必须回到 64 位 HotSpot 的位布局本身,理解 tag bits 的动态偏移逻辑,才能明白指针字段的边界在哪里、为什么开启指针压缩后行为会变化。

一、64 位 Mark Word 的位布局与动态偏移的含义
在 64 位 JVM 上,一个对象头的 Mark Word 占 8 个字节,共 64 个 bit。这 64 个 bit 并不是固定划分的,而是根据低位的锁标志位动态解释剩余字段的含义。最后 2 个 bit 是锁标志位(lock bits),倒数第 3 个 bit 是 biased_lock 位,这两部分合起来构成了对象处于哪种状态的解释依据。
为什么说是动态偏移?因为不同状态下,剩余 61 个 bit 的字段划分完全不同。无锁状态下,前面 25 个 bit 未使用,接着 31 个 bit 存 identity_hashcode,再往后是分代年龄。而在轻量级锁状态下,高 62 个 bit 全部变成了指向栈中 Lock Record 的指针。到了重量级锁,高 62 个 bit 则指向堆中的 ObjectMonitor 对象。同一个物理位置,在不同状态下被解释成完全不同的字段,这就是“动态位偏移”的本质。
关键在于代码里并不存在“从第几位到第几位是某字段”的硬编码偏移。HotSpot 通过位运算和常量掩码来提取字段,比如判断是否为重量级锁,就是检查 (mark & 0b111) == 0b010。锁标志为 10 时,JVM 知道这 62 个 bit 是一个指针,直接把 Mark Word 右移后强转即可拿到 ObjectMonitor 的地址。理解了这一点,后续的关联机制就顺理成章了。
二、重量级锁的膨胀过程与 ObjectMonitor 的分配时机
重量级锁不是凭空出现的。当轻量级锁在自旋失败或发生锁竞争后,会触发锁膨胀(inflation)。膨胀过程发生在 ObjectSynchronizer::inflate 方法中,它的工作可以概括为几步:首先通过 CAS 或原子操作在堆上分配一个 ObjectMonitor 对象;然后把这个 ObjectMonitor 的地址写入 Mark Word 的高 62 位,同时把锁标志位设置为 10;最后如果有线程正在持有该轻量级锁,还会把这个线程包装成一个 ObjectWaiter 挂到 monitor 的等待队列里。
这里有一个容易被忽略的细节:ObjectMonitor 的分配是懒加载式的。JVM 维护着一个全局的 monitor 监视池(ObjectMonitorFreeList),膨胀时优先从空闲链表中取一个复用,取不到才真正 new 一个。这也是为什么频繁的锁膨胀不会立刻把堆撑爆——监视器对象是可以回收再分配的。只有当膨胀失败(哈希表打满)时才会抛出 OutOfMemoryError。
膨胀成功之后,对象头与 ObjectMonitor 的关联就建立起来了。此后任何线程走到 monitorenter 字节码指令,解释器或 JIT 生成的代码都会先读对象头,判断锁标志位是 10,然后从 Mark Word 中解析出 monitor 指针,接着进入 ObjectMonitor::enter 的慢路径逻辑。
三、ObjectMonitor 内部结构与关联后的执行路径
拿到 monitor 指针之后,竞争就完全发生在 ObjectMonitor 内部了。它的核心字段可以简化成下面的结构:
// HotSpot 中 ObjectMonitor 的关键字段(简化)
class ObjectMonitor {
void* volatile _owner; // 持有锁的线程或 BasicLock
volatile intptr_t _recursions; // 重入计数
ObjectWaiter* volatile _EntryList; // 阻塞队列(竞争失败者)
ObjectWaiter* volatile _cxq; // 最近到达的竞争队列
Thread* volatile _succ; // 假定的继任者(减少不必要的唤醒)
volatile int _WaitSetLock; // 保护 _WaitSet 的自旋锁
ObjectWaiter* volatile _WaitSet; // wait() 等待队列
volatile markWord _header; // 保存膨胀前的 Mark Word
};其中 _recursions 字段解释了可重入性:同一个线程再次 enter 时,发现 _owner 就是自己,计数加一,直接返回,不需要再走队列。_header 字段则保存了膨胀前的 Mark Word 快照,这非常关键——锁释放后对象要回到无锁或轻量级状态,靠的就是这份备份。如果膨胀前对象已经计算过 identity_hashcode,这个值也被记录在 _header 里,避免锁状态切换导致哈希码丢失。
竞争失败的线程会经历一个精心设计的路径:先尝试 CAS 进入 _cxq 队列头部,然后执行若干次自旋(自适应自旋),期待持有者很快释放;自旋失败后才调用 pthread 的条件变量真正挂起线程,这也是重量级锁开销大的根源——涉及内核态的上下文切换。唤醒时线程也不是直接获得锁,而是从 _cxq 或 _EntryList 中被移出后重新竞争,体现了非公平锁的语义。
四、用实验验证 Mark Word 与 monitor 的关联
理解了原理,最好亲手验证。JOL(Java Object Layout)是观察对象头布局最方便的工具。写一段代码制造重量级锁,然后打印对象头:
import org.openjdk.jol.info.ClassLayout;
public class HeavyLockDemo {
static final Object lock = new Object();
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(() -> {
synchronized (lock) {
try { Thread.sleep(2000); } catch (Exception e) {}
}
});
t1.start();
Thread.sleep(500); // 确保 t1 已进入同步块
Thread t2 = new Thread(() -> {
synchronized (lock) {
System.out.println("t2 got lock");
}
});
t2.start();
t2.join();
// 输出 Mark Word,观察锁标志位
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
}运行后观察输出的 Mark Word 行,当锁竞争发生并膨胀后,最后 3 位会呈现 010 的组合(10 为重量级锁标志,biased_lock 位为 0),而前面的十六进制数值就是 ObjectMonitor 在堆中的地址。你可以在不同运行间对比这个值,会发现同一把锁在多次膨胀中地址可能不同——这正是监视池复用机制的表现。
另外可以对比开启与关闭指针压缩(-XX:-UseCompressedOops)时的输出。理论上重量级锁指针占用的是 Mark Word 的高 62 位,与压缩指针无关,因为压缩Oops机制只作用于对象引用字段,而 monitor 指针是原生地址直接存储的。亲自做一次对比实验,能帮助你彻底区分“对象引用的压缩”和“Mark Word 内部指针的存储方式”这两个容易混淆的概念,把对 Mark Word 动态布局的理解落到可观察的证据上。
Mark Word重量级锁ObjectMonitor修改时间:2026-09-04 17:48:47