重量级锁触发后Mark Word末3位为010仅是入口信号,真正标志是其余61位构成可解析的ObjectMonitor*地址;JOL显示0x0000001000000000时末3位实为000,说明未膨胀,需确保真实线程竞争且禁用偏向锁。

重量级锁触发后 Mark Word 末 3 位变成 010,但真正关键的是它存的不是“锁状态”,而是 ObjectMonitor* 地址
很多人以为看到 010 就等于“锁已膨胀”,其实这只是入口信号。真正要确认是否进入重量级锁,得看这 8 字节(64 位)里除末 3 位外的其余位是否构成一个合法的内存地址——而且这个地址必须能被 HotSpot 解析为 ObjectMonitor 实例。
常见错误现象:JOL 打印出的十六进制值末三位是 010,但高位全是 0(比如 0x0000000000000010),这说明锁还没真正膨胀,只是刚尝试升级、尚未完成 Monitor 分配;或者 JVM 启用了 -XX:+UseBiasedLocking 但未禁用,导致偏向锁未及时撤销,阻塞了后续升级路径。
- 必须确保测试中至少有两个线程对同一对象执行
synchronized,且存在真实竞争(例如加Thread.sleep(1)或循环等待) - 避免 JIT 编译优化掉同步块:用
volatile引用对象、放入List、或在同步块内外都调用System.out - 64 位 JVM 默认开启指针压缩(
-XX:+UseCompressedOops),此时ObjectMonitor*是 32 位压缩地址,需左移 3 位再还原为真实地址(JOL 不自动做这步,得手动处理)
用 ClassLayout.parseInstance(obj).toPrintable() 查到的十六进制值,怎么拆解出 Monitor 地址
JOL 默认只输出对象头的原始字节布局,不解释语义。比如打印出:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 0x00000000
4 4 (object header) 0x00000010这是 8 字节合并后的结果(小端序),实际应拼为 0x0000001000000000(注意高低位顺序)。取低 3 位得 000?不对——要按 HotSpot 源码约定,**从最低位开始数:bit0–bit2 是 lock bits**,所以 0x0000001000000000 的二进制末尾是 000?再检查:十六进制 00 对应二进制 00000000,末三位确实是 000。说明当前还不是重量级锁——你可能漏掉了竞争条件。
- 正确做法:把整个 8 字节转成 64 位二进制字符串,截取
substring(0, 61)(去掉末 3 位 lock bits),再转回 long,就是 Monitor 指针值(压缩地址需 - 验证地址有效性:用
jhsdb jmap --pid <pid> --binaryheap导出堆快照,搜索该地址是否落在ObjectMonitor实例范围内(需配合hsdis或调试版 JVM) - 若地址值极小(如
0x10、0x20)或为全 0,基本可判定 Monitor 尚未分配,锁仍卡在轻量级锁自旋阶段
ObjectMonitor 地址写入 Mark Word 的时机,比你想象中更“懒”
HotSpot 并不会在第一次检测到竞争时就立即分配 ObjectMonitor。它会先尝试自旋(spin)、再尝试轻量级锁重试,只有当自旋失败 + 多次重试失败 + 确认有线程进入 EntryList 阻塞时,才调用 ObjectSynchronizer::inflate() 分配 Monitor 并更新 Mark Word。
- 这意味着:即使两个线程同时执行
synchronized(obj),只要其中一个快速退出,另一个可能全程走轻量级锁路径,Mark Word 始终保持000(轻量级锁标志位) - 用
jstack -l <pid>查看线程栈,若看到- waiting to lock <0x...>,且该地址与 JOL 打印出的高位一致,才是 Monitor 已生效的铁证 - 禁用自旋(
-XX:PreBlockSpin=0)和禁用偏向锁(-XX:-UseBiasedLocking)能加速膨胀过程,方便观察
为什么有时候 010 出现了,但 ObjectMonitor._owner 却是 null
这是最易被忽略的细节:Monitor 分配完成 ≠ 锁已被持有。ObjectMonitor 是一个独立结构体,包含 _owner、_EntryList、_WaitSet 等字段。Mark Word 写入 Monitor 地址,只是建立了对象与 Monitor 的关联;真正设置 _owner 是在 ObjectMonitor::enter() 中通过 CAS 完成的。
所以你会看到一种中间态:Mark Word 已含有效 Monitor 地址(末三位 010),但 _owner == NULL——说明 Monitor 已就位,但还没有线程成功获取它。此时新来的线程会直接进入 _EntryList 阻塞,而不是再去 inflate。
- 用
jhsdb clhsdb连上运行中的 JVM,执行inspect <monitor_addr>可查看_owner字段值 - 如果
_owner是非空但指向一个已销毁线程(如thread_id = 0),说明发生过锁竞争但 Owner 已退出,Monitor 处于“待回收”状态(HotSpot 会延迟回收以减少开销) - 这种分离设计正是锁升级机制的弹性所在:Monitor 分配与锁获取解耦,避免每次竞争都触发内存分配
真实场景下,Mark Word 和 ObjectMonitor 的联动不是原子的一锤定音,而是一组带状态跃迁的协作步骤。想靠一次 JOL 输出就断言“已重量级锁”,大概率会误判——重点不是末三位是不是 010,而是那个地址背后有没有活着的 ObjectMonitor,以及它里面有没有正在起作用的 _owner。


















