HotSpot的轻量锁与膨胀后的ObjectMonitor状态重建原理剖析
- 前言
- 轻量锁与膨胀后的ObjectMonitor状态重建原理
-
- [1. Deopt 锁重建的总体拓扑与处理逻辑](#1. Deopt 锁重建的总体拓扑与处理逻辑)
- [2. HotSpot C++ 源代码详细分析](#2. HotSpot C++ 源代码详细分析)
-
- 步骤一:编译帧解包与锁分类 (`vframeArray.cpp`)
- 步骤二:重新锁定被消除的锁 (`deoptimization.cpp`)
- [步骤三:`LockStack` 补加锁与强行膨胀逻辑 (`lightweightSynchronizer.cpp`)](#步骤三:
LockStack补加锁与强行膨胀逻辑 (lightweightSynchronizer.cpp))
- [3. 两种物理锁状态(`LockStack` vs. `ObjectMonitor`)的重建细节](#3. 两种物理锁状态(
LockStackvs.ObjectMonitor)的重建细节) -
- [场景 A:轻量锁状态(Fast-Locked `000` & 在 `LockStack` 中)](#场景 A:轻量锁状态(Fast-Locked
000& 在LockStack中)) - [场景 B:膨胀锁状态(Inflated `10` & `ObjectMonitor` 持有)](#场景 B:膨胀锁状态(Inflated
10&ObjectMonitor持有))
- [场景 A:轻量锁状态(Fast-Locked `000` & 在 `LockStack` 中)](#场景 A:轻量锁状态(Fast-Locked
- [4. 关键边缘情况与约束保障](#4. 关键边缘情况与约束保障)
-
- [1. `LockStack` 溢出(Capacity Boundary = 8)](#1.
LockStack溢出(Capacity Boundary = 8)) - [2. Deopt 过程中的 GC Safepoint 与 Oop 映射](#2. Deopt 过程中的 GC Safepoint 与 Oop 映射)
- [1. `LockStack` 溢出(Capacity Boundary = 8)](#1.
- [5. 状态转换总结矩阵](#5. 状态转换总结矩阵)
前言
本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限,文中内容难免存在疏漏,恳请读者不吝指正。
轻量锁与膨胀后的ObjectMonitor状态重建原理
当 C2 发生 Deoptimization(解优化)时,HotSpot 是如何处理 LockStack 中的轻量锁与膨胀后的 ObjectMonitor 状态重建的。
在 JDK 21 引入 -XX:+UseLightweightLocking 机制后,HotSpot JVM 的锁架构发生了重大调整:BasicLock(栈锁记录)不再保存 Displaced Mark Word,线程持有的轻量级锁统一由 JavaThread::_lock_stack(Thread-Local 的对象指针数组,容量上限为 8)管理。
当 C2 编译器执行 Deoptimization(解优化) 时,编译栈帧(Compiled Frame)被解包(Unpack)并重建为一至多个解释器栈帧(Interpreter Frame)。此时,HotSpot 必须依据 C2 在编译期记录的调试元数据(ScopeDesc -> MonitorValue),重建这些解释器帧对应的锁物理状态(LockStack 或 ObjectMonitor)与逻辑句柄(BasicObjectLock)。
1. Deopt 锁重建的总体拓扑与处理逻辑
在 Deoptimization 过程中,C2 编译帧中的锁分为两类,其重建路径存在本质区别:
┌──────────────────────────────────────────────┐
│ C2 Compiled Frame Deoptimization │
│ (vframeArray::allocate_array & unpack) │
└──────────────────────┬───────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 解析 ScopeDesc 中的 MonitorValue │
└──────────────────────┬───────────────────────┘
│
┌───────────────────────────────┴───────────────────────────────┐
│ │
▼ (mv->is_eliminated() == true) ▼ (mv->is_eliminated() == false)
┌──────────────────────────────────────────────┐ ┌──────────────────────────────────────────────┐
│ 被 C2 消除的锁 (Eliminated Lock) │ │ 物理已持有的锁 (Physical Lock) │
├──────────────────────────────────────────────┤ ├──────────────────────────────────────────────┤
│ 物理上未加锁(不在 LockStack/Monitor 中) │ │ 物理上已加锁(在 LockStack 或 Monitor 中) │
│ 记录至 deferred_joins 延迟链表 │ │ 保持当前 Mark Word 和 LockStack 物理状态不变 │
│ 触发 Deoptimization::relock_objects │ │ 仅在解包的解释器帧中构建 BasicObjectLock 槽位 │
│ 调用 LightweightSynchronizer::enter_for │ │ 使 BasicObjectLock::_obj 指向该 oop │
└──────────────────────┬───────────────────────┘ └──────────────────────┬───────────────────────┘
│ │
▼ ▼
┌──────────────────────────────────────────────┐ ┌──────────────────────────────────────────────┐
│ 重新分配物理锁凭证: │ │ 物理状态一致性对齐: │
│ 1. 若 LockStack 未满且无竞争: │ │ a) 若处于 Fast-Locked (000): │
│ - CAS 置 markWord 为 Fast-Locked (000) │ │ - LockStack 保留指针; │
│ - 压入 JavaThread._lock_stack │ │ - 解释器 exit 时直接从 LockStack 弹出 │
│ 2. 若 LockStack 已满(>=8)或发生竞争: │ │ b) 若处于 Inflated (10): │
│ - 强行膨胀为 ObjectMonitor │ │ - ObjectMonitor::_owner 保持当前线程; │
│ - 置 markWord 为 Inflated (10) │ │ - 解释器 exit 时走向 ObjectMonitor::exit │
└──────────────────────┬───────────────────────┘ └──────────────────────┬───────────────────────┘
│ │
└───────────────────────────────┬────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ Reconstructed Interpreter Frames │
│ 解释器字节码对齐完成,可以安全执行 monitorexit│
└──────────────────────────────────────────────┘
2. HotSpot C++ 源代码详细分析
解优化过程中的锁状态重建分布在 vframeArray.cpp、deoptimization.cpp 和 lightweightSynchronizer.cpp 三个核心源文件中。
步骤一:编译帧解包与锁分类 (vframeArray.cpp)
在 C2 解包的第一阶段,vframeArrayElement::unpack_on_stack 遍历 ScopeDesc 中的 MonitorValue。此处决定哪些锁需要物理重建,哪些锁仅需建立解释器帧指针映射。
cpp
// src/hotspot/share/runtime/vframeArray.cpp
void vframeArrayElement::unpack_on_stack(int caller_actual_parameters,
int callee_parameters,
int callee_locals,
methodHandle method,
int is_top_frame,
int is_bottom_frame,
int exec_mode) {
// ...
// 遍历编译期记录的所有锁元数据 (MonitorValue)
for (int index = 0; index < monitors()->length(); index++) {
MonitorValue* mv = monitors()->at(index);
ScopeValue* owner_sv = mv->owner();
// 1. 从 C2 作用域值中提取当前持有的 Java 对象 (oop)
StackValue* value = StackValue::create_stack_value(frame_map, reg_map, owner_sv);
Handle obj(Thread::current(), value->get_obj()());
// 2. 在物理重建的解释器栈帧中预留 BasicObjectLock 槽位
BasicObjectLock* sc = stack_child->interpreter_frame_monitor_begin() - index - 1;
sc->set_obj(obj()); // 将解释器帧的 monitor 关联指向该 oop
// 3. 区分"被 C2 逃逸分析消除的锁"与"物理已持有的锁"
if (mv->is_eliminated()) {
// 被消除的锁在硬件物理上并没有加锁(Mark Word 仍为 Unlocked,LockStack 中无此 oop)。
// 必须加入 deferred_joins 队列,延迟到所有栈帧解包完成后,统一通过 relock_objects 重新加锁。
deferred_joins->append(new monitorArrayElement(obj(), sc->lock()));
} else {
// 物理已持有的锁:在 C2 运行期间已经执行过物理加锁(即对象已经在 LockStack 中,
// 或 Mark Word 已经为 Fast-Locked 000 / Inflated 10)。
// 此时【完全不需要】重新 CAS 或修改 LockStack,其物理锁凭证依然有效!
}
}
// ...
}
步骤二:重新锁定被消除的锁 (deoptimization.cpp)
对于 mv->is_eliminated() == true 的锁,在解释器接管运行前必须强行补做加锁操作,否则解释器后续执行 monitorexit 字节码时会报 IllegalMonitorStateException。
cpp
// src/hotspot/share/runtime/deoptimization.cpp
void Deoptimization::relock_objects(JavaThread* thread, GrowableArray<monitorArrayElement*>* objects) {
assert(objects != nullptr, "must be");
for (int i = 0; i < objects->length(); i++) {
monitorArrayElement* mon = objects->at(i);
Handle obj(thread, mon->owner());
BasicLock* lock = mon->lock();
// 在 JDK 21 -XX:+UseLightweightLocking 模式下,调用 LightweightSynchronizer 重建物理锁状态
if (UseLightweightLocking) {
LightweightSynchronizer::enter_for(obj, lock, thread);
} else {
// 传统路径 (Displaced Mark Word / Legacy Heavyweight Lock)
ObjectSynchronizer::enter(obj, lock, thread);
}
}
}
步骤三:LockStack 补加锁与强行膨胀逻辑 (lightweightSynchronizer.cpp)
LightweightSynchronizer::enter_for 是 JDK 21 Deopt 重建物理锁凭证的核心入口:
cpp
// src/hotspot/share/runtime/lightweightSynchronizer.cpp
void LightweightSynchronizer::enter_for(Handle obj, BasicLock* lock, JavaThread* current) {
assert(current->is_Java_thread(), "must be JavaThread");
markWord mark = obj->mark();
// -------------------------------------------------------------------------
// 情况 1:对象处于无锁状态 (Unlocked 001)
// -------------------------------------------------------------------------
if (mark.is_unlocked()) {
// 检查当前线程的 Thread-Local LockStack 是否有剩余容量 (容量上限为 8)
if (current->lock_stack().can_push()) {
// 尝试通过 CAS 将 Mark Word 从 Unlocked (001) 切换为 Fast-Locked (000)
markWord new_mark = markWord::prototype().set_fast_locked();
if (obj->cas_set_mark(new_mark, mark) == mark) {
// CAS 成功:将 oop 压入当前 JavaThread 的 _lock_stack 中!
current->lock_stack().push(obj());
return; // 成功恢复为轻量级锁 (LockStack 状态)
}
}
}
// -------------------------------------------------------------------------
// 情况 2:LockStack 已满 (>= 8) 或发生并发竞争 / 对象已被膨胀
// -------------------------------------------------------------------------
// 如果 LockStack 容量溢出或 CAS 失败,不能丢弃锁,必须退化为锁膨胀 (Inflate)
ObjectMonitor* monitor = ObjectSynchronizer::inflate(current, obj(), inflate_cause_vm_internal);
// 强行把 ObjectMonitor 的 _owner 指向当前 Deopt 的线程,完成膨胀锁重建
monitor->enter(current);
}
3. 两种物理锁状态(LockStack vs. ObjectMonitor)的重建细节
针对在 Deopt 前物理上已经持有的锁,HotSpot 如何保证物理状态与解包后的解释器帧平滑对接:
场景 A:轻量锁状态(Fast-Locked 000 & 在 LockStack 中)
- 物理凭证保持 :在 C2 编译代码执行期间,
fast_lock汇编指令已经将oop压入了JavaThread::_lock_stack,Mark Word 为000。 - Deopt 动作:
- 不会触发
enter_for,_lock_stack的内容保持原封不动。 - 解包逻辑仅需在 Interpreter Frame 的栈底填入
BasicObjectLock结构,将_obj属性指向该oop。
- **解释器后续执行
monitorexit**:
- 解释器执行到
monitorexit时,通过LightweightSynchronizer::exit慢速/快速路径,直接调用current->lock_stack().remove(obj())将该oop从_lock_stack中移除,并通过 CAS 将对象 Mark Word 恢复为Unlocked (001)。
场景 B:膨胀锁状态(Inflated 10 & ObjectMonitor 持有)
- 物理凭证保持 :对象可能在 C2 运行期间调用了
Object.wait()、计算了identityHashCode或发生了多线程争用,已膨胀为ObjectMonitor。对象的 Mark Word 指向ObjectMonitor*,且ObjectMonitor::_owner == thread。
- 注意 :在 JDK 21 中,**膨胀后的对象指针会被移出
LockStack**,不再占用LockStack的 8 个槽位。
- Deopt 动作:
ObjectMonitor实例及其_owner保持不变。- Interpreter Frame 分配
BasicObjectLock槽位,关联该oop。
- **解释器后续执行
monitorexit**:
- 解释器读取 Mark Word 低 2 位发现为
10 (has_monitor),直接调用ObjectMonitor::exit,减少重入计数或释放_owner。
4. 关键边缘情况与约束保障
1. LockStack 溢出(Capacity Boundary = 8)
如果一个方法在 C2 阶段消除了 10 个对象的锁(例如在循环中对非逃逸对象加锁),当发生 Deopt 时,Deoptimization::relock_objects 依次调用 LightweightSynchronizer::enter_for:
- 前 8 个对象通过 CAS 被设置为
Fast-Locked (000)并压入LockStack。 - 第 9 个和第 10 个对象调用
enter_for时,current->lock_stack().can_push()返回false。 enter_for**自动触发ObjectSynchronizer::inflate**,将第 9 和第 10 个对象膨胀为ObjectMonitor,将_owner设为当前线程。这完美解决了 Thread-LocalLockStack容量有限的问题。
2. Deopt 过程中的 GC Safepoint 与 Oop 映射
在解包 C2 栈帧构建解释器帧及调用 relock_objects 期间,对象句柄全部通过 Handle(thread, oop) 包装。如果 enter_for 中发生的 ObjectSynchronizer::inflate 触发了 C-Heap 申请或 GC,GC 根扫描能够正确更新 LockStack 和 Handle 中的对象指针,防止悬挂指针(Dangling Pointer)。
5. 状态转换总结矩阵
| 锁在 C2 编译帧中的原始状态 | 物理 Mark Word (低 3 位) | JavaThread::_lock_stack 状态 |
Deopt 解包时的核心操作 | 解包后的解释器执行行为 |
|---|---|---|---|---|
| 被 C2 锁消除 (Eliminated) | 001 (Unlocked) |
不存在该 oop |
调用 relock_objects -> enter_for 补加锁 |
重新压入 LockStack(若满了则膨胀为 ObjectMonitor) |
| 未消除轻量锁 (Fast-Locked) | 000 (Fast-Locked) |
已经存在该 oop |
不修改物理状态 ,仅构建解释器 BasicObjectLock 映射 |
解释器 monitorexit 时从 LockStack 弹出 oop |
| 未消除膨胀锁 (Inflated) | 10 (Inflated) |
不存在该 oop |
不修改物理状态 ,保留 ObjectMonitor::_owner |
解释器 monitorexit 时走 ObjectMonitor::exit |