纯轻量级锁体系下C2编译器锁粗化和消除机制剖析
- 前言
- 纯轻量级锁体系下C2编译器锁粗化和消除机制
-
- [1. 锁消除(Lock Elimination):从"逻辑消除"到"零物理栈槽(Zero Stack Slot)"](#1. 锁消除(Lock Elimination):从“逻辑消除”到“零物理栈槽(Zero Stack Slot)”)
-
- [1.1 `BoxLockNode` 栈物理分配解耦](#1.1
BoxLockNode栈物理分配解耦) - [1.2 Deoptimization(解优化)的虚拟重构机制变迁](#1.2 Deoptimization(解优化)的虚拟重构机制变迁)
- [1.1 `BoxLockNode` 栈物理分配解耦](#1.1
- [2. 锁粗化(Lock Coarsening):开销模型的剧变与消除收益放大](#2. 锁粗化(Lock Coarsening):开销模型的剧变与消除收益放大)
-
- [2.1 C2 锁粗化模式匹配与别名校验](#2.1 C2 锁粗化模式匹配与别名校验)
- [2.2 宏展开阶段节点直接裁撤(Macro Expansion)](#2.2 宏展开阶段节点直接裁撤(Macro Expansion))
- [3. 两种锁体系下 C2 锁优化特性的全面对比](#3. 两种锁体系下 C2 锁优化特性的全面对比)
- [4. 汇编代码生成(x86_64)物理指令消减对比](#4. 汇编代码生成(x86_64)物理指令消减对比)
-
- 锁粗化失效(生成未优化机器码)
- [经过 C2 锁粗化优化(经过 `disconnect_projections` 裁剪)](#经过 C2 锁粗化优化(经过
disconnect_projections裁剪))
- 总结
前言
本文旨在记录近期研读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 架构下,未粗化的连续轻量级锁包含"双重物理开销":
- 总线/硬件级原子指令 :针对 Mark Word 的
lock cmpxchgl指令。 - 线程局部内存写 :对
JavaThread::_lock_stack的_top指针及_base[]数组元素的更新(占用 L1 数据缓存与 Store Buffer)。
这一改变使得 C2 编译器的锁粗化优化收益相比以往显著放大。
2.1 C2 锁粗化模式匹配与别名校验
在 IGVN 与 Loop Transformations 阶段,C2 对连续的 FastUnlockNode 与 FastLockNode 进行检测:
源码文件: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 栈指针的频繁写入。