04-LockStack在虚拟线程Mount和Unmount拷贝过程剖析
前言
本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限,文中内容难免存在疏漏,恳请读者不吝指正。
LockStack在虚拟线程Mount和Unmount拷贝过程
在 JDK 21+ 引入的轻量级锁(Fast Locking / JEP 428)架构中,HotSpot 废弃了传统依赖 Native 栈帧上 BasicObjectLock / Lock Record 链表的实现,改用直接嵌入 JavaThread 的 LockStack 数据结构。
当虚拟线程(VirtualThread)在 Carrier 线程(JavaThread)上执行 Mount (Thaw) 与 Unmount (Freeze) 时,LockStack 内部管理的对象指针(oop)必须在 Carrier 线程本地空间 与 **堆区 stackChunkOop** 之间进行剥离与还原。
一、 LockStack 核心 C++ 数据结构定义
LockStack 物理上直接内嵌在 JavaThread 对象中,避免了动态内存分配。
1. LockStack 结构定义 (src/hotspot/share/runtime/lockStack.hpp)
cpp
class LockStack {
public:
static const int CAPACITY = 8; // 线程本地轻量级锁栈容量(默认为 8)
private:
// 直接存储锁定对象的 oop(指向 Java 堆对象的指针)
oop _base[CAPACITY];
// 栈顶指针,指示下一个空闲槽位的 offset (0 ~ CAPACITY)
uint8_t _top;
public:
LockStack() : _top(0) {
#ifdef ASSERT
for (int i = 0; i < CAPACITY; i++) {
_base[i] = nullptr;
}
#endif
}
// 基础压栈与出栈
void push(oop obj) {
assert(_top < CAPACITY, "LockStack overflow");
_base[_top++] = obj;
}
oop pop() {
assert(_top > 0, "LockStack underflow");
oop obj = _base[--_top];
_base[_top] = nullptr;
return obj;
}
// 从栈中特定位置移除锁对象(支持非严格 LIFO 的解锁顺序)
void remove(oop obj);
// 判断对象是否在当前线程的 LockStack 中
bool contains(oop obj) const;
// 暴露给 Freeze / Thaw 的指针与 Offset 接口
oop* base() { return _base; }
uint8_t top() const { return _top; }
void set_top(uint8_t top) { _top = top; }
};
2. 在 JavaThread 中的内存内联 (src/hotspot/share/runtime/javaThread.hpp)
cpp
class JavaThread : public Thread {
// ...
private:
LockStack _lock_stack; // 嵌入到 JavaThread 实例中的物理对象
public:
LockStack& lock_stack() { return _lock_stack; }
static ByteSize lock_stack_offset() {
return byte_offset_of(JavaThread, _lock_stack);
}
};
二、 Freeze (Unmount) 阶段:LockStack 的剥离与迁移
在虚拟线程挂起(Freeze)时,系统必须将 Carrier 线程 JavaThread::_lock_stack 中属于当前 Continuation 栈帧范围内的 oop 提取并转移 到堆上的 stackChunkOop,同时清理 Carrier 线程的 LockStack。
1. 挂起检查与 Pinning 校验 (continuationFreeze.cpp)
在执行数据拷贝前,HotSpot 首先通过 Freeze::check_pinned() 确认锁状态:
cpp
// src/hotspot/share/runtime/continuationFreeze.cpp
template <typename Config>
class Freeze : public StackObj {
JavaThread* const _thread; // 当前 Carrier 线程
ContinuationWrapper& _cont; // 关联的 Continuation
bool check_pinned() {
// 1. 若使用了膨胀锁(Heavyweight Monitor),直接锁定 Pinning
if (_thread->has_pending_monitors()) {
return true; // 无法 Unmount
}
// 2. 检索 LockStack 中是否有受包含锁
LockStack& lock_stack = _thread->lock_stack();
if (lock_stack.top() > 0) {
// 遍历 LockStack,校验锁定对象对应的栈帧是否在 Freeze 作用域内
for (int i = 0; i < lock_stack.top(); i++) {
oop obj = lock_stack.base()[i];
if (is_locked_in_chunk_scope(obj)) {
// 在 JDK 21 中属于轻量级锁且符合条件的锁允许转移,无需 Pin
}
}
}
return false;
}
};
2. LockStack 提取与数据写入 C++ 源码
在确认可冻结后,通过 Freeze::freeze_lock_stack 进行数据剥离:
cpp
// src/hotspot/share/runtime/continuationFreeze.cpp (概念化精简实现)
template <typename Config>
void Freeze::freeze_lock_stack(stackChunkOop chunk) {
LockStack& lock_stack = _thread->lock_stack();
int top = lock_stack.top();
if (top == 0) return;
int chunk_lock_count = 0;
// 遍历 Carrier 线程当前的 LockStack
for (int i = top - 1; i >= 0; i--) {
oop obj = lock_stack.base()[i];
// 校验该锁关联的栈帧是否属于即将打入 stackChunkOop 的范围
if (_cont.is_in_frame_range(obj)) {
// 1. 将 oop 写入 stackChunkOop 的锁对象存储区 (Lock Payload Zone)
chunk->write_lock_at(chunk_lock_count++, obj);
// 2. 从 Carrier 线程的 LockStack 中弹出/移除该对象
lock_stack.remove(obj);
}
}
// 更新 stackChunkOop 头部元数据中的锁计数器
chunk->set_lock_count(chunk_lock_count);
}
剥离关键要点:
- 必须物理移除 :若不清空 Carrier 线程
JavaThread::_lock_stack中的oop,后续其他虚拟线程被 Mount 到该 Carrier 上执行monitorenter时,会误以为自己已经持有该锁,导致并发保护机制失效。 - 数据流向 :
JavaThread::_lock_stack→\to→stackChunkOop::lock_data()(堆内存连续区域)。
三、 Thaw (Mount) 阶段:LockStack 的还原与重新填充
当虚拟线程被 ForkJoinPool 的某一个 Carrier 线程选中,准备恢复执行(Thaw)时,系统需要将暂存在 stackChunkOop 里的 oop 重新还原 到新 Carrier 线程的 LockStack 中。
1. LockStack 还原 C++ 源码 (continuationThaw.cpp)
cpp
// src/hotspot/share/runtime/continuationThaw.cpp
template <typename Config>
void Thaw::thaw_lock_stack(stackChunkOop chunk) {
JavaThread* thread = _thread; // 当前接管执行的 Carrier 线程
LockStack& lock_stack = thread->lock_stack();
int chunk_lock_count = chunk->lock_count();
if (chunk_lock_count == 0) return;
// 确认 Carrier 线程的 LockStack 是否有足够空间(CAPACITY = 8)
assert(lock_stack.top() + chunk_lock_count <= LockStack::CAPACITY,
"Carrier LockStack overflow during thaw!");
// 从 stackChunkOop 依次读取并重新压入 Carrier 线程的 LockStack
for (int i = 0; i < chunk_lock_count; i++) {
oop obj = chunk->read_lock_at(i);
if (obj != nullptr) {
// 压入当前 Carrier 线程本地的 LockStack
lock_stack.push(obj);
}
}
// 清空 stackChunkOop 中的暂存锁记录,防止 GC 重复扫描
chunk->set_lock_count(0);
}
还原关键要点:
- 跨 Carrier 迁移 :Mount 时的 Carrier 线程物理上可能与 Unmount 时的 Carrier 线程完全不同。通过
thaw_lock_stack,锁持有者上下文无缝切换到了新的JavaThread。 - 次序一致性:压栈顺序必须严格保持原有的嵌套锁定层级(如外层锁先入栈、内层锁后入栈)。
四、 完整 C++ 内存数据流动图解
[ Carrier Thread A (JavaThread A) ] [ Java Heap (stackChunkOop) ]
+------------------------------------+ +-------------------------------+
| _lock_stack: | | stackChunkOop |
| _base[0] = objA (Outer Lock) | | _lock_count = 0 |
| _base[1] = objB (Inner Lock) | | _lock_payload[] = { null } |
| _top = 2 | +-------------------------------+
+-----------------+------------------+ ^
| |
| ====== [ Freeze / Unmount 剥离 ] =========|
| 1. 将 objA, objB 拷贝至 stackChunkOop
| 2. 执行 lock_stack.remove() 清空 Carrier A
v
+------------------------------------+ +-------------------------------+
| Carrier Thread A (JavaThread A) | | stackChunkOop |
| _lock_stack: | | _lock_count = 2 |
| _base[0] = null | | _lock_payload[0] = objA |
| _top = 0 (变为空闲状态) | | _lock_payload[1] = objB |
+------------------------------------+ +-------------------------------+
|
|
[ Carrier Thread B (JavaThread B) ] |
+------------------------------------+ |
| Carrier Thread B (准备接管) | |
| _lock_stack: | |
| _top = 0 | |
+-----------------+------------------+ |
| |
| ====== [ Thaw / Mount 还原 ] =============|
| 1. 从 stackChunkOop 读取 objA, objB
| 2. 执行 lock_stack.push() 还原至 Carrier B
v
+------------------------------------+ +-------------------------------+
| Carrier Thread B (JavaThread B) | | stackChunkOop |
| _lock_stack: | | _lock_count = 0 (重置) |
| _base[0] = objA | | _lock_payload[] = { null } |
| _base[1] = objB | +-------------------------------+
| _top = 2 |
+------------------------------------+
五、 GC 屏障(OopMap)与指针自愈配合
当虚拟线程处于 Unmount 挂起状态时,锁对象 objA 与 objB 仅被存储在堆内的 stackChunkOop 中:
- GC Root 注册 :
stackChunkOop作为标准堆对象,在其OopMap中注册了内部保存的lock_payload偏移量。 - 并发标记与对象移动(以 ZGC / G1 为例):
- 若 GC 在虚拟线程挂起期间触发了垃圾回收或对象压缩,GC 遍历到
stackChunkOop时,会自动更新lock_payload中存放的oop物理地址(转向移动后的新地址)。
- Thaw 时获取自愈指针 :当虚拟线程在 Thaw 阶段恢复时,
chunk->read_lock_at(i)读取到的已经是经过 GC 修正、指向有效新内存地址的指针,直接写入JavaThread::_lock_stack即可,无需额外的指针修正步骤。