Markword在紧凑对象头上的实现原理剖析
- 前言
- Markword在紧凑对象头上的实现原理
-
- [传统偏向锁/轻量级锁对 Mark Word 的物理覆写困境](#传统偏向锁/轻量级锁对 Mark Word 的物理覆写困境)
-
- 传统轻量级锁的覆写机制 (HotSpot Legacy)
- [阻碍 Compact Object Headers (Lilliput) 的根本矛盾](#阻碍 Compact Object Headers (Lilliput) 的根本矛盾)
- [LockStack 如何实现"解耦"并腾出 Bit 空间](#LockStack 如何实现“解耦”并腾出 Bit 空间)
- [HotSpot 源码级详细剖析](#HotSpot 源码级详细剖析)
-
- [1. `LockStack` 线程局部数据结构](#1.
LockStack线程局部数据结构) - [2. Project Lilliput 中的 Mark Word 位图结构定义](#2. Project Lilliput 中的 Mark Word 位图结构定义)
- [3. LightweightSynchronizer 加锁与解锁代码路径](#3. LightweightSynchronizer 加锁与解锁代码路径)
- [1. `LockStack` 线程局部数据结构](#1.
- [C2 编译器与 JIT 汇编级 Fast-Path 展开](#C2 编译器与 JIT 汇编级 Fast-Path 展开)
-
- [C2 生成的 FastLock 机器码指令逻辑](#C2 生成的 FastLock 机器码指令逻辑)
- 新旧轻量级锁机制对比
- 总结
前言
本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限,文中内容难免存在疏漏,恳请读者不吝指正。
Markword在紧凑对象头上的实现原理
传统偏向锁/轻量级锁对 Mark Word 的物理覆写困境
在 JDK 21 引入 Thread-Local Lock Stack (LockStack) 以及 Project Lilliput (JEP 450) 落地前,HotSpot JVM 的传统轻量级锁(BasicLock / Displaced Mark Word 机制)在对象头空间分配上存在无法调和的物理冲突。
传统轻量级锁的覆写机制 (HotSpot Legacy)
在传统 64 位 HotSpot 架构中,对象的 Mark Word 占据 64 位,其布局随着锁状态动态变化。当一个对象处于无锁状态(Unlocked, 001)时,Mark Word 存放 HashCode、GC Age 等信息;一旦被线程通过 CAS 加轻量级锁,Mark Word 的全部 64 位将被整体替换。
在 src/hotspot/share/oops/markWord.hpp 的传统定义中:
cpp
// 传统 64 位 Mark Word 位图结构:
// Unlocked: [ unused:25 | identity_hash:31 | age:4 | biased_lock:1 | lock:2 (01) ]
// Lightweight: [ ptr_to_basic_lock:62 | lock:2 (00) ]
// Inflated: [ ptr_to_object_monitor:62 | lock:2 (10) ]
当加锁时,VM 会在当前 Java 线程的 C-Stack 栈帧中分配一个 BasicLock(包含 _displaced_header),并将对象的原始 Mark Word 复制到该栈帧槽位中。随后,通过 CAS 操作将对象的 Mark Word 替换为指向该 BasicLock 的 64 位内存指针:
cpp
// src/hotspot/share/runtime/synchronizer.cpp (Legacy Fast Enter)
void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {
markWord mark = obj->mark();
if (mark.is_unlocked()) {
// 1. 将原始 Mark Word 暂存到 C-Stack 的 BasicLock 槽位
lock->set_displaced_header(mark);
// 2. 将整个 Mark Word 编码为指向 BasicLock 的物理地址指针 (高 62 位被完全占满)
markWord locked_mark = markWord::encode_pointer_as_mark(lock);
if (obj->cas_set_mark(locked_mark, mark) == mark) {
return; // 加锁成功
}
}
...
}
阻碍 Compact Object Headers (Lilliput) 的根本矛盾
Project Lilliput 的目标是将 64 位 Mark Word 与 32/64 位 Klass*(类指针)合并为单个 64 位甚至 32 位的 Compact Header 。在 Lilliput 布局中,压缩类指针 nKlass 被强制静态嵌入在 Mark Word 的固定 Bit 位区间内(例如高 32 位或中间 22~32 位)。
如果继续沿用传统的轻量级锁机制,一旦发生轻量级加锁,markWord::encode_pointer_as_mark(lock) 会将 对象头的全部高 62 位完全覆盖 为栈地址:
+-------------------------------------------------------------------+
| 传统加锁:62 位栈地址 (BasicLock*) | State |
+-------------------------------------------------------------------+
▲
└─ 这种覆盖直接抹去了嵌入在 Mark Word 内部的 nKlass (类指针)!
在轻量级锁持有期间,任何依赖 oopDesc::klass() 的高频操作(如 invokevirtual 虚方法查找、instanceof 类型检查、GC 标记扫描)都无法直接从对象头读取 nKlass,而必须沿着 Mark Word 中的指针回溯到线程 C-Stack 帧的 BasicLock::_displaced_header 中解引用还原 Klass*。这在多线程高并发场景下会导致物理内存回溯访问的高昂开销,使对象头压缩失去实用价值。
LockStack 如何实现"解耦"并腾出 Bit 空间
为了彻底打破"锁指针覆盖 Mark Word"的枷锁,JDK 21 引入了全新的轻量级锁实现(JEP 450 前置依赖:Fast Lightweight Locking),其核心概念是 Thread-Local Lock Stack (LockStack)。
LockStack 改变了锁所有权的记录逻辑:取消把线程栈地址写回 Mark Word 的做法,改由 JavaThread 结构内部的线程私有固定数组维护锁对象引用;Mark Word 在加锁期间仅修改极少数 State 状态位,内部包含的 nKlass 以及 HashCode/Age 位空间原位保留,不发生任何破坏性覆写。
Thread-Local Lock Stack 架构解耦
JavaThread (r15 寄存器直接定位)
+------------------------------------+
| ... |
| _lock_stack: |
| [ index 0 ]: oop_A (对象A指针) |
| [ index 1 ]: oop_B (对象B指针) | ──┐
| [ index 2 ]: NULL | │ 记录锁所有权
| _top: 2 | │ (不占用 Mark Word 空间)
+------------------------------------+ │
│
v
Java Object Header (Mark Word)
+-------------------------------------------------------------------+
| Hash/Age/Self-Lock Bits | nKlass (22-32b) | Lock State (00)|
+-------------------------------------------------------------------+
^^^^^^^^^^^^^^^^^^
nKlass 原位保留,未被物理指针覆写!
HotSpot 源码级详细剖析
在 JDK 21 及 Lilliput 分支的 OpenJDK 源码中,该机制的实现分布在 lockStack.hpp、markWord.hpp 和 lightweightSynchronizer.cpp 等模块中。
1. LockStack 线程局部数据结构
在 src/hotspot/share/runtime/lockStack.hpp 中,HotSpot 将 LockStack 作为一个连续的内存数组嵌入在 JavaThread 对象内部。
cpp
// src/hotspot/share/runtime/lockStack.hpp
class LockStack {
friend class VMStructs;
friend class BytecodeInterpreter;
friend class C2FastLockNode;
public:
// 锁栈固定容量 (8 个槽位,覆盖 99.9% 以上的嵌套锁场景)
static const int CAPACITY = 8;
private:
// _top 指向下一个可插入的数组索引(以 byte offset 形式存储以便汇编定位)
uint32_t _top;
// 存放已加锁对象的 oop 物理指针数组
oop _base[CAPACITY];
public:
LockStack() : _top(offset_to_index(base_offset())) {}
// 判断锁栈是否已满
bool is_full() const {
return _top >= CAPACITY;
}
// 入栈操作:线程成功 CAS 加锁后调用
void push(oop o) {
assert(!is_full(), "lock stack overflow");
assert(_base[_top] == nullptr, "must be empty");
_base[_top] = o;
_top++;
}
// 检查当前线程是否持有该对象的锁
bool contains(oop o) const {
for (int i = _top - 1; i >= 0; i--) {
if (_base[i] == o) {
return true;
}
}
return false;
}
// 出栈操作:解锁时调用
oop pop() {
assert(_top > 0, "lock stack underflow");
_top--;
oop o = _base[_top];
_base[_top] = nullptr;
return o;
}
};
设计精髓
- CPU Cache Line 友好 :
_base数组存在于线程本身的JavaThread内存空间中,在 x86_64 下通过%r15(Thread Register)或 AArch64 下的x28直接寻址,访问速度极快。 - 物理脱耦 :锁所有权由
contains(oop)隐式证明。如果对象的 Mark Word 标志位处于 Lightweight Locked 状态,且当前线程的LockStack包含该对象地址,则当前线程独占该锁。Mark Word 内部完全不需要保存任何指向线程或栈的物理指针。
2. Project Lilliput 中的 Mark Word 位图结构定义
由于 LockStack 接管了锁所有权的物理存储,Mark Word 腾出了超过 60 位的空闲空间。Project Lilliput 在 src/hotspot/share/oops/markWord.hpp 中定义了紧凑对象头的位布局。
cpp
// src/hotspot/share/oops/markWord.hpp (Lilliput Branch / Compact Object Headers)
class markWord {
private:
uintptr_t _value;
public:
// ------------------------------------------------------------------------
// Lilliput 64 位 Compact Header 位分布规约:
//
// 位 0 - 2 : Lock State Bits (锁状态标志位)
// 001: Unlocked (无锁)
// 000: Fast-Locked (Lightweight Locked, 使用 LockStack)
// 010: Inflated Monitor (膨胀锁,指向 ObjectMonitor)
// 011: Marked for GC (GC 标记)
// 位 3 - 6 : GC Age (对象年龄)
// 位 7 : Self-Lock / Hash-State Bits
// 位 8 - 39 : nKlass (32-bit Compressed Klass Pointer 压缩类指针)
// 位 40 - 63: Identity Hashcode (24-bit 散列码或衍生标记)
// ------------------------------------------------------------------------
static const int lock_shift = 0;
static const int lock_bits = 3;
static const uintptr_t lock_mask = right_n_bits(lock_bits);
static const int age_shift = lock_bits;
static const int age_bits = 4;
// nKlass 存放区间:在加锁全生命周期中永不被锁清零或覆盖
static const int nklass_shift = lock_bits + age_bits + 1; // Bit 8 开始
static const int nklass_bits = 32; // 占用 32 位
static const uintptr_t nklass_mask = right_n_bits(nklass_bits);
// 判断是否为 LockStack 管理的 Fast-Locked 状态
bool is_fast_locked() const {
return (value() & lock_mask) == lock_state_fast_locked; // 即 000 状态
}
// 极速提取 Klass* 指针(即使在 Fast-Locked 状态下依然有效)
narrowKlass narrow_klass() const {
return narrowKlass(assert_unsigned_field_overflow(value() >> nklass_shift) & nklass_mask);
}
};
3. LightweightSynchronizer 加锁与解锁代码路径
在 JDK 21 中,旧版的 ObjectSynchronizer::fast_enter 被全新的 LightweightSynchronizer 替换。源码位于 src/hotspot/share/runtime/lightweightSynchronizer.cpp。
加锁实现:LightweightSynchronizer::enter
cpp
// src/hotspot/share/runtime/lightweightSynchronizer.cpp
void LightweightSynchronizer::enter(Handle obj, BasicLock* lock, JavaThread* current) {
markWord mark = obj->mark();
// 1. 无锁状态 (Unlocked, 001) 下的 Fast-Path 处置
if (mark.is_unlocked()) {
// 构造快速加锁的目标 markWord:仅将锁标志位设置为 000 (Fast-Locked)
// 注意:markWord 内部包含的 nKlass、Age、Hash 绝对数值完全保持原样!
markWord locked_mark = mark.set_fast_locked();
// 原子 CAS 替换 Mark Word (只修改状态位,不做指针覆写)
markWord old_mark = obj->cas_set_mark(locked_mark, mark);
if (old_mark == mark) {
// CAS 成功!将对象指针推入当前线程的 LockStack
current->lock_stack().push(obj());
return; // 加锁成功,直接返回,没有物理 BasicLock 指针写入
}
mark = old_mark;
}
// 2. 锁重入 (Reentrant Lock) 判定
if (mark.is_fast_locked() && current->lock_stack().contains(obj())) {
// 如果当前线程已经持有该对象的锁,支持递归加锁
// 只需要向线程的 LockStack 中再次压入该 oop 即可,Mark Word 无需做任何修改
if (!current->lock_stack().is_full()) {
current->lock_stack().push(obj());
return;
}
// 若 LockStack 已满 (超过 8 层重入),退化降级处理,触发锁膨胀
}
// 3. 慢速路径 (Slow Path):竞争失败或 LockStack 溢出,膨胀为重量级锁 (ObjectMonitor)
enter_slow(obj, lock, current);
}
解锁实现:LightweightSynchronizer::exit
cpp
// src/hotspot/share/runtime/lightweightSynchronizer.cpp
void LightweightSynchronizer::exit(oop obj, current) {
markWord mark = obj->mark();
// 如果处于 Fast-Locked 状态
if (mark.is_fast_locked()) {
// 1. 优先尝试从 LockStack 的栈顶弹出该对象
if (current->lock_stack().peek() == obj) {
current->lock_stack().pop();
// 检查当前线程的 LockStack 中是否还存在该对象的重入锁
if (current->lock_stack().contains(obj)) {
return; // 仍有外层 synchronized 块持有该锁,解锁直接结束
}
// 2. 还原 Mark Word 为 Unlocked (001) 状态
markWord unlocked_mark = mark.set_unlocked();
if (obj->cas_set_mark(unlocked_mark, mark) == mark) {
return; // CAS 成功,解锁完成
}
}
}
// 锁膨胀后的慢速解锁路径
exit_slow(obj, current);
}
C2 编译器与 JIT 汇编级 Fast-Path 展开
在底层内联汇编(以 x86_64 为例,src/hotspot/cpu/x86/c2_MacroAssembler_x86.cpp)中,LockStack 展现出了极高的执行效率。C2 不再需要像旧版本那样在当前栈帧的 BasicLock 中保存 Displaced Header,从而减少了一次 Stack Memory Store。
C2 生成的 FastLock 机器码指令逻辑
assembly
# 输入: %r12 = 对象指针 (oop), %r15 = 当前 JavaThread 指针
# 输出: ZF (Zero Flag) 标志位表示 CAS 是否成功
# 1. 读取当前对象的 Mark Word
movq 0x0(%r12), %rax # %rax = Mark Word
# 2. 测试锁标志位是否为 Unlocked (001)
testq $0x7, %rax # 检查低 3 位
jnz .L_slow_path # 非 001 状态进入 Slow Path
# 3. 构造 Locked Mark Word: 保持高位 nKlass/Hash 不变,仅将低 3 位转为 000
movq %rax, %rbx
andq $~0x7, %rbx # %rbx = Target Mark Word (Fast-Locked: 000)
# 4. 执行原子 CAS 指令
lock cmpxchgq %rbx, 0x0(%r12) # 比较并交换 Mark Word
jnz .L_slow_path # CAS 冲突进入 Slow Path
# 5. CAS 成功:将对象写入 JavaThread 的 LockStack
movl 0x2e0(%r15), %ecx # 读取 thread->_lock_stack._top offset (假设偏移 0x2e0)
movq %r12, 0x2e4(%r15, %rcx, 8) # lock_stack._base[top] = oop
addl $1, 0x2e0(%r15) # _top++
# 完成加锁,没有对物理 C-Stack 帧写入 Displaced Header!
新旧轻量级锁机制对比
下表直观总结了为何 LockStack 能够成为 Lilliput 实现 Compact Object Headers 的决定性基石:
| 维度 | 传统轻量级锁 (BasicLock) |
JDK 21+ LockStack (Fast Locking) |
|---|---|---|
| ** Mark Word 占用机制** | 强行覆盖 Mark Word 高 62 位 为物理栈地址 | 零指针覆盖 ,仅更新低 3 位锁状态标志位(001 → \rightarrow → 000) |
nKlass(类指针)存储 |
被强行抹除,必须沿着栈指针去 BasicLock 盲追 |
永久驻留 在 Mark Word 固定的 Bit 区间内,物理上永不毁损 |
getClass() 读取开销 |
加锁期间必须回溯 C-Stack 取回原始 Mark Word | O ( 1 ) O(1) O(1) 无锁读取 ,仅需按位右移与掩码(mark >> nklass_shift & mask) |
| 锁所有权记载位置 | 对象的 Mark Word 内部(存栈地址) | 线程私有的 JavaThread::_lock_stack 线程局部数组中 |
| 锁重入处理方式 | 在 Stack Frame 压入 NULL 头的 BasicLock 槽 |
直接在 LockStack 数组尾部 append 目标对象 oop |
| CPU Cache 影响 | 频繁写入当前栈帧 BasicLock 内存页 |
极佳的 Cache Line 局部性,完全在 %r15 线程结构缓存行内完成 |
| 对 Compact Header 的支持 | 物理拒绝 (不可能在 64 位内容纳 nKlass) |
完备支持(Project Lilliput JEP 450 核心依赖) |
总结
Project Lilliput 的本质是在有限的 64 位(甚至 32 位)Word 空间内,以精细化 Bit 拼图的形式重新构建 Java 对象头。
HotSpot 放弃传统的 BasicLock 栈地址覆盖机制,转向基于 LockStack 的 Thread-Local 锁跟踪 ,将锁所有权状态从"对象头记载"解耦为"线程局部数组记载"。这一架构变革彻底释放了 Mark Word 中被物理指针霸占的 62 位空间,使压缩类指针 nKlass 能够平滑、永久地嵌入到对象头中,从底层奠定了 64 位紧凑对象头(Compact Object Headers)的根基。