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


前言

本文旨在记录近期研读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.hppmarkWord.hpplightweightSynchronizer.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)的根基。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值