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

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 的传统定义中:

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.hppmarkWord.hpplightweightSynchronizer.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)的根基。

相关推荐
君顾11 小时前
24小时自助健身房系统开发实战与完整指南
java·开发语言·健身房
潘潘的嵌入式日记1 小时前
位操作怎么写才不出错?置位、清位、翻转、取位一次讲清
c语言·嵌入式·寄存器·位操作
鹿角片ljp1 小时前
LeetCode 78:子集|回溯、选与不选、递归和path快照
java·数据结构·算法
酸菜。1 小时前
DW_apb_wdt总结
linux
虚无的纽扣1 小时前
【Linux】一篇文章带你吃透进程控制:fork、wait、exit到底在做什么
linux·ubuntu
raindayinrain1 小时前
Linux 网络编程--套接字选项
linux·网络·套接字选项
hansang_IR2 小时前
【代数与组合数学 | 那忘算 5】生成函数 & 例题 & 卷积
c++·算法·多项式·生成函数·母函数
无忧.芙桃2 小时前
数据结构之排序算法(上):从评价指标到插入、希尔、选择与堆排序
c语言·c++·排序算法
金玉满堂@bj2 小时前
多环境部署方案(开发、测试、生产,搭配Tomcat\+WAR包)
java·tomcat·maven