纯轻量级锁体系下C2编译器锁粗化和消除机制剖析

纯轻量级锁体系下C2编译器锁粗化和消除机制剖析


前言

本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限,文中内容难免存在疏漏,恳请读者不吝指正。

纯轻量级锁体系下C2编译器锁粗化和消除机制

在 JDK 21+ 引入纯轻量级锁(Fast Lightweight Locking / LockStack)体系及 Project Lilliput 后,偏向锁(Biased Locking)彻底清退,且轻量级锁不再将 Displaced Mark Word 写入 C-Stack 的 BasicLock 槽位。这使得 C2 编译器的锁消除(Lock Elimination)锁粗化(Lock Coarsening)机制在 IR 节点展开、物理栈帧分配、Deoptimization 解优化以及机器码生成等层面发生了深刻的底层重构。


1. 锁消除(Lock Elimination):从"逻辑消除"到"零物理栈槽(Zero Stack Slot)"

在传统 BasicLock 体系下,即便逃逸分析(Escape Analysis, EA)判定对象未逃逸并触发锁消除,C2 在 Parsing 阶段生成的 BoxLockNode 仍需在当前 Java 栈帧上强行分配 8~16 字节的物理内存空间(用于存放 Displaced Mark Word 和对象指针),以备运行时发生解优化(Deoptimization)时恢复对象头。

在纯 LockStack 体系下,锁消除实现了彻底的 C-Stack 内存剥离。

1.1 BoxLockNode 栈物理分配解耦

在 C2 宏节点展开阶段(Macro Expansion),标记为 eliminated 的锁节点在剔除控制与内存依赖流的同时,彻底取消了后端对 BoxLockNode 的物理栈槽映射。

源码文件:src/hotspot/share/opto/macroLock.cpp
cpp 复制代码
void PhaseMacroExpand::eliminate_locking_node(AbstractLockNode *alock) {
  if (!alock->is_eliminated()) {
    if (alock->is_coarsened()) {
      alock->set_eliminated(); // 锁粗化同样走锁消除的图裁撤路径
    }
  }

  if (alock->is_eliminated()) {
    // 1. 在 Sea-of-Nodes 图中将当前 Lock/Unlock 节点从 Control 和 Memory 链旁路 (Bypass)
    disconnect_projections(alock);

    // 2. 获取锁绑定的 BoxLockNode
    BoxLockNode* box = alock->box_node()->as_BoxLock();
    if (box->is_eliminated()) {
      // 在纯 LockStack 体系下,PhaseOutput (Code Generation) 识别到 box->is_eliminated() 后,
      // 彻底跳过 Frame Map 中的 Stack Slot 内存分配;
      // 该 BoxLock 节点仅作为 ScopeDesc 元数据留存,不产生任何栈开销指令。
    }
  }
}

void PhaseMacroExpand::disconnect_projections(AbstractLockNode* alock) {
  Node* ctrl = alock->in(TypeFunc::Control);
  Node* mem  = alock->in(TypeFunc::Memory);

  // 遍历 Lock/Unlock 节点的所有输出 Projection
  for (DUIterator_Fast imax, i = alock->fast_outs(imax); i < imax; i++) {
    Node* use = alock->fast_out(i);
    if (use->is_proj()) {
      if (use->as_Proj()->_con == TypeFunc::Control) {
        _igvn.replace_node(use, ctrl); // 后继 Control 直接接入前驱 Control
      } else if (use->as_Proj()->_con == TypeFunc::Memory) {
        _igvn.replace_node(use, mem);  // 后继 Memory 直接接入前驱 Memory
      }
    }
  }
}

1.2 Deoptimization(解优化)的虚拟重构机制变迁

当已被消除锁的 JIT 编译帧触发 Deoptimization(如标量替换失败或捕获 Uncommon Trap)时,JVM 必须在栈帧还原过程中重新建立加锁状态。由于没有物理 C-Stack 槽位备份 Displaced Mark Word,HotSpot 的 Deoptimizer 调整为向线程私有 LockStack 重新压栈。

源码文件:src/hotspot/share/runtime/deoptimization.cpp
cpp 复制代码
void Deoptimization::relock_objects(JavaThread* thread, GrowableArray<MonitorValue*>* monitors) {
  for (int i = 0; i < monitors->length(); i++) {
    MonitorValue* mon_info = monitors->at(i);
    ScopeValue* obj_sv = mon_info->owner();
    oop obj = ...; // 从 Frame 恢复出物理 oop

    // 纯 LockStack 体系下的还原逻辑:
    // 不再写 C-Stack 帧的 BasicLock,而是直接插入当前线程的 JavaThread::_lock_stack
    thread->lock_stack().push(obj);

    // 将对象头 Mark Word 原位修改为 Fast-Locked 状态 (保留包含 nKlass 在内的 Bit 位)
    markWord mark = obj->mark();
    if (mark.is_unlocked()) {
      obj->set_mark(mark.set_fast_locked());
    }
  }
}

2. 锁粗化(Lock Coarsening):开销模型的剧变与消除收益放大

偏向锁清退后,任何未消除的无竞争锁都必须走轻量级锁路径。在 LockStack 架构下,未粗化的连续轻量级锁包含"双重物理开销"

  1. 总线/硬件级原子指令 :针对 Mark Word 的 lock cmpxchgl 指令。
  2. 线程局部内存写 :对 JavaThread::_lock_stack_top 指针及 _base[] 数组元素的更新(占用 L1 数据缓存与 Store Buffer)。

这一改变使得 C2 编译器的锁粗化优化收益相比以往显著放大。

2.1 C2 锁粗化模式匹配与别名校验

在 IGVN 与 Loop Transformations 阶段,C2 对连续的 FastUnlockNodeFastLockNode 进行检测:

源码文件:src/hotspot/share/opto/compile.cpp
cpp 复制代码
bool Compile::coarsen_locks_check(AbstractLockNode* unlock, AbstractLockNode* lock) {
  Node* obj1 = unlock->obj_node()->uncast();
  Node* obj2 = lock->obj_node()->uncast();

  // 1. 节点同一性检查:判断 GVN 图中锁对象引用指针是否相同
  if (obj1 == obj2) {
    return true;
  }

  // 2. 别名类型(Alias Type)分析:校验是否指向同一别名类的指针
  const Type* t1 = _igvn.type(obj1);
  const Type* t2 = _igvn.type(obj2);
  if (t1 == t2 && t1->isa_oopptr()) {
    return true; // 确认锁对象一致,准许粗化
  }

  return false;
}

2.2 宏展开阶段节点直接裁撤(Macro Expansion)

当判定两个锁节点满足粗化条件时,C2 调用 set_coarsened() 标记,随后在 expand_locknode() 中跳过物理加/解锁指令的生成:

源码文件:src/hotspot/share/opto/macroLock.cpp
cpp 复制代码
void PhaseMacroExpand::expand_locknode(LockNode *lock) {
  if (lock->is_coarsened()) {
    // 被标记为 coarsened 的锁节点不再生成任何 FastLock 机器码,
    // 直接调用 eliminate_locking_node 从 IR 控制流/内存流中裁剪
    eliminate_locking_node(lock);
    return;
  }

  // 只有未被粗化的外层大锁节点,才会最终进入 MacroAssembler::fast_lock 发射汇编指令
  ...
}

3. 两种锁体系下 C2 锁优化特性的全面对比

优化维度 传统 BasicLock 体系 (JDK 8 / 11) LockStack 体系 (JDK 21+ / Project Lilliput)
锁消除 (EA) 物理栈开销 需在 Stack Frame 分配 BasicLock 空间,写 Displaced Mark Word 零 Stack Slot 分配,彻底从 C-Stack 帧销毁物理空间占用。
锁粗化 (Lock Coarsening) 收益 在偏向锁生效时,粗化仅消减重入计数,物理收益平缓。 同时消除物理 CAS (lock cmpxchgl) 指令与 LockStack 数组内存 Store 操作,收益大幅提升。
Deoptimization 恢复机制 解析 ScopeDesc,重建 BasicLock 结构并还原 Displaced Mark Word 到物理栈帧。 运行 thread->lock_stack().push(obj),将对象直接写回线程局部锁栈。
对象头 nKlass 提取影响 锁未消除/未粗化时,BasicLock 覆写 Mark Word,提取 Klass* 需追溯栈指针。 锁粗化与否不影响 nKlass 提取 ;Mark Word 内的 nKlass Bit 位绝对固定,单次位移掩码即可解出。

4. 汇编代码生成(x86_64)物理指令消减对比

以一段连续加锁代码为例:

java 复制代码
public void execute(Object lock) {
    synchronized(lock) { this.varA = 1; }
    synchronized(lock) { this.varB = 2; }
}

锁粗化失效(生成未优化机器码)

assembly 复制代码
# --- 第一次加锁 ---
mov    0x0(%rsi), %rax           # 读取 Compact Header (Mark Word)
test   $0x1, %al                 # 校验 Unlocked 状态
jz     .L_slow_path_1
mov    %rax, %rbx
and    $~0x7, %rbx
lock cmpxchg %rbx, 0x0(%rsi)     # 【1】物理 CAS 原子指令 (总线锁定)
jnz    .L_slow_path_1
mov    0x280(%r15), %ecx         # 读取 JavaThread::_lock_stack._top
mov    %rsi, 0x288(%r15,%rcx,8)  # 【2】写 LockStack 数组
add    $0x1, %ecx
mov    %ecx, 0x280(%r15)         # 更新 _top

movl   $0x1, 0x10(%rdi)          # this.varA = 1

# --- 第一次解锁 ---
sub    $0x1, %ecx                # 【3】LockStack 出栈操作
mov    %ecx, 0x280(%r15)
...                              # (CAS 修改锁标志位为 Unlocked)

# --- 第二次加锁 (未粗化导致重复开销) ---
lock cmpxchg %rbx, 0x0(%rsi)     # 【4】重复的物理 CAS
mov    %rsi, 0x288(%r15,%rcx,8)  # 【5】重复写 LockStack

movl   $0x2, 0x14(%rdi)          # this.varB = 2

经过 C2 锁粗化优化(经过 disconnect_projections 裁剪)

assembly 复制代码
# --- 合并后的单一临界区加锁 ---
mov    0x0(%rsi), %rax
test   $0x1, %al
jz     .L_slow_path
mov    %rax, %rbx
and    $~0x7, %rbx
lock cmpxchg %rbx, 0x0(%rsi)     # 【仅执行 1 次物理 CAS】
mov    0x280(%r15), %ecx
mov    %rsi, 0x288(%r15,%rcx,8)  # 【仅执行 1 次 LockStack 压栈】
add    $0x1, %ecx
mov    %ecx, 0x280(%r15)

# --- 合并后的临界区业务代码 ---
movl   $0x1, 0x10(%rdi)          # this.varA = 1
movl   $0x2, 0x14(%rdi)          # this.varB = 2

# --- 合并后的单一临界区解锁 ---
sub    $0x1, %ecx                # 【仅执行 1 次 LockStack 出栈】
mov    %ecx, 0x280(%r15)
...

总结

在纯 LockStack 轻量级锁体系下,C2 编译器的锁优化机制完成了关键蜕变:锁消除(Lock Elimination)彻底斩断了传统 BasicLock 对 C-Stack 帧内存槽位的依赖,消除了物理栈分配开销;锁粗化(Lock Coarsening)则充当了偏向锁废弃后的核心性能屏障,同时削减了物理 CPU 级的原子 lock 前缀指令与线程局部 LockStack 栈指针的频繁写入。

相关推荐
运维全栈笔记1 小时前
Nginx 模块化多业务站点通用配置模板
linux·运维·nginx
ss2731 小时前
AI全栈实战 | 1.4-02 Spring AOP:@Transactional 失效的 N 种场景,根因其实只有一个
java·spring·ai编程
weilx12341 小时前
C++笔记-mutex
c++
辛迪聊物业数字化1 小时前
【拆解智慧物业管理系统架构:从SaaS落地到IoT全链路实现】
大数据·linux·组合模式
beijixinghe1 小时前
第15节 指针作为函数参数的工程实战用法
开发语言·c++·c++基础·c++入门·几何引擎c++
梦想平凡1 小时前
情怀棋牌源代码焕新记录(一):全新国风UI与原版工程梳理
java·服务器·数据库·websocket·网络协议·cocos2d
binqian1 小时前
【java】Java 两种锁机制与线程阻塞唤醒原理
jvm·spring boot·spring
j7~1 小时前
【C++微服务项目开发脚手架】(准备篇)从 0 到跑通:Docker 容器开发环境 + 宿主机用户 + Trae 远程连接 全链路踩坑实战
linux·c++·学习·xshell·docker容器·trae
程序员-Benothing2 小时前
Linux用户与组管理命令大全:useradd、usermod、groupadd、passwd
linux·运维·服务器