云计算百科
云计算领域专业知识百科平台

Markword在紧凑对象头上的实现原理剖析

Markword在紧凑对象头上的实现原理剖析

  • 前言
  • Markword在紧凑对象头上的实现原理
    • 传统偏向锁/轻量级锁对 Mark Word 的物理覆写困境
      • 传统轻量级锁的覆写机制 (HotSpot Legacy)
      • 阻碍 Compact Object Headers (Lilliput) 的根本矛盾
    • LockStack 如何实现“解耦”并腾出 Bit 空间
    • HotSpot 源码级详细剖析
      • 1. `LockStack` 线程局部数据结构
        • 设计精髓
      • 2. Project Lilliput 中的 Mark Word 位图结构定义
      • 3. LightweightSynchronizer 加锁与解锁代码路径
        • 加锁实现:`LightweightSynchronizer::enter`
        • 解锁实现:`LightweightSynchronizer::exit`
    • C2 编译器与 JIT 汇编级 Fast-Path 展开
      • 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 的传统定义中:

// 传统 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 位内存指针:

// 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 对象内部。

// 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 中定义了紧凑对象头的位布局。

// 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

// 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

// 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 机器码指令逻辑

# 输入: %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)的根基。

赞(0)
未经允许不得转载:网硕互联帮助中心 » Markword在紧凑对象头上的实现原理剖析
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!