线程进入safepoint机制解析
线程进入Safepoint机制概述
并不是所有的 Java 线程都在执行 JIT 编译后的机器码。JVM 将执行状态分为以下四大类,它们进入 Safepoint 的机制各不相同:
| 线程当前执行状态 | 进入 Safepoint 的机制原理 |
|---|---|
| Compiled (JIT 编译码) | 执行到由 C1/C2 插入的 Polling 指令,触发 SIGSEGV 信号中断进入。 |
| Interpreted (解释执行) | 字节码解释器(Template Interpreter)拥有一张派发表(Dispatch Table)。当触发 Safepoint 时,VM Thread 会把正常的派发表替换为 Safepoint 专用派发表。这样解释器在执行下一条字节码指令时,会自动路由到安全点处理代码。 |
| Native (JNI 运行中) | 执行 Native 代码的线程不需要立即挂起,因为 Native 代码不会直接修改 Java 堆对象。VM Thread 会直接将其标记为“已处于安全点”。但当该 Native 线程执行完毕,准备**返回 Java 世界(Transition Back)**时,会在边界处检查 Safepoint 状态,若仍处于 STW,则在该边界处被挂起。 |
| Blocked (阻塞/睡眠状态) | 线程处于 Object.wait()、Thread.sleep() 或等待锁(Mux/Lock)状态。这类线程同 Native 一样,被 VM 视为默认安全。它们在被唤醒或者获取到锁准备恢复运行的瞬间,也会进行边界检查并阻塞。 |
处于Blocked(阻塞/睡眠状态)线程进入safepoint机制解析
在 OpenJDK 8 中,当 JVM 发起 Safepoint(全局安全点)请求时,正在执行 Java 字节码的线程需要通过主动轮询(Polling)来响应。然而,对于处于 Blocked(阻塞/睡眠,JVM 内部对应 _thread_blocked 状态) 的线程,其进入 Safepoint 的机制采用了“被动确认”与“主动拦截”相结合的不对称设计。
简而言之:VM Thread 在发起 Safepoint 时,直接单方面认为 _thread_blocked 状态的线程已经处于 Safepoint;而该线程一旦试图唤醒并退出阻塞状态,则必须在边界处进行拦截,由其自身完成逻辑上的“进入”与等待。
1. 核心机制:非对称的安全状态确认
当线程处于 _thread_blocked 状态时(例如:因为 Object.wait()、Thread.sleep()、等待进入 synchronized 块或等待本地 IO 阻塞),它绝对不会修改 Java 堆内存,也不会执行任何可能改变 Java 线程上下文的字节码。
因此,HotSpot 认为该线程是天然安全的。
整个过程分为两个视角:
- VM Thread 视角:直接将该线程标记为
safe,无需等待其响应,直接推进 Safepoint 流程。 - Blocked 线程视角:在阻塞结束、准备恢复执行前,必须强制检查全局 Safepoint 状态。如果全局 Safepoint 正在进行,该线程将自我阻塞,直到 Safepoint 结束。
2. VM Thread 视角的源码分析:安全状态判定
当 VM Thread 执行 SafepointSynchronize::begin() 试图让所有线程进入 Safepoint 时,它会遍历所有的 JavaThread,并通过 safepoint_safe 函数评估线程当前的状态。
源码路径:share/vm/runtime/safepoint.cpp
// 检查特定线程在当前状态下是否对 Safepoint 安全
inline bool SafepointSynchronize::safepoint_safe(JavaThread *thread, JavaThreadState state) {
switch(state) {
case _thread_in_native:
// JNI 线程同样被视为安全
return true;
case _thread_blocked:
// 【关键点】如果线程处于 _thread_blocked 状态,VM Thread 直接返回 true!
// 这意味着 VM Thread 不需要向该线程发送任何信号,也不需要等待它到达轮询点。
// 在 VM Thread 眼里,这个线程此时此刻已经处于 Safepoint 中。
return true;
default:
return false;
}
}
在 SafepointSynchronize::begin() 的主循环中,正是通过这个判定来减少等待计数的:
void SafepointSynchronize::begin() {
// ... 省略前置初始化代码 ...
// 悬挂全局 Safepoint 状态,通知全系统
_state = _synchronizing;
OrderAccess::fence(); // 确保可见性
// 循环等待所有非安全线程到达 Safepoint
int iterations = 0;
while (!is_all_java_threads_frozen()) {
int still_running = 0;
for (JavaThread *p = Threads::first(); p != NULL; p = p->next()) {
JavaThreadState state = p->thread_state();
// 检查线程是否安全
if (safepoint_safe(p, state)) {
// 如果是 _thread_blocked,直接视作已进入 Safepoint,不计入 still_running
continue;
}
// 如果不是安全状态(如 _thread_in_Java),则需要等待,计数加 1
still_running++;
}
if (still_running == 0) {
break; // 所有线程都安全了,成功达成 Safepoint
}
// 如果还有线程在运行,VM Thread 适当挂起等待
os::naked_yield();
}
// 进入同步完成状态
_state = _synchronized;
}
3. Blocked 线程视角的源码分析:退出时的状态拦截
既然 VM Thread 直接放行了 Blocked 线程,那么如果该线程在 Safepoint 期间突然被唤醒(例如 sleep 时间到了,或者持锁线程释放了锁),它如果直接继续执行,就会破坏 Safepoint 的独占性(如引发 GC 时的对象移动问题)。
因此,HotSpot 在所有状态切换的边界处设计了状态转换宏(State Transitions)。当 Blocked 线程试图切回运行状态(如 _thread_in_vm 或 _thread_in_Java)时,会被强制拦截。
源码路径:share/vm/runtime/interfaceSupport.hpp
以典型的 ThreadBlockInVM 动作为例,它利用 C++ 的 RAII 机制,在析构函数(即退出 Blocked 状态时)强制检查 Safepoint:
class ThreadBlockInVM : public ThreadStateTransition {
public:
ThreadBlockInVM(JavaThread *thread) : ThreadStateTransition(thread) {
// 进入构造函数:从 _thread_in_vm 切换为 _thread_blocked 状态
trans_and_fence(_thread_in_vm, _thread_blocked);
}
~ThreadBlockInVM() {
// 【关键点】析构函数:当阻塞结束,线程试图离开 _thread_blocked 状态时
// 强制调用转换函数,检查是否需要因 Safepoint 而拦截自己
trans_from_java_blocked(_thread_in_vm);
}
};
我们深入看 trans_from_java_blocked 的实现,它展示了拦截的底层逻辑:
void trans_from_java_blocked(JavaThreadState to) {
// 1. 尝试将状态修改为目标状态(如 _thread_in_vm)
_thread->set_thread_state(to);
// 2. 内存屏障,确保状态立即对 VM Thread 可见
OrderAccess::fence();
// 3. 【核心拦截检查】检查当前 JVM 是否正在进行 Safepoint 同步
// do_call_back() 会检查全局的 Safepoint 状态是否为 _synchronizing 或 _synchronized
if (SafepointSynchronize::do_call_back()) {
// 如果有 Safepoint 正在发生,绝不允许放行!
// 必须调用 block() 将自己真正挂起,退回到 Safepoint 等待队列中
SafepointSynchronize::block(_thread);
}
}
源码路径:share/vm/runtime/safepoint.cpp 中的 block()
当上述拦截命中后,线程会进入 SafepointSynchronize::block 函数。在这里,它会把自己的状态再次认领为 _thread_blocked,然后死锁在 Safepoint_lock 上。
void SafepointSynchronize::block(JavaThread *thread) {
// 获取当前线程保存的原本目标状态
JavaThreadState state = thread->thread_state();
// 【重新声明安全】为了防止在拿锁过程中全局 Safepoint 仍在推进,
// 重新将自己的状态强制设置为 _thread_blocked,告诉 VM Thread:我很安全,不用管我。
thread->set_thread_state(_thread_blocked);
// 竞争互斥锁 Safepoint_lock
MutexLocker mu(Safepoint_lock);
// 循环检查:只要全局 Safepoint 没有结束,就在这里长眠
while (SafepointSynchronize::is_synchronizing() || SafepointSynchronize::is_synchronized()) {
// 挂起当前线程,等待 VM Thread 在 Safepoint 结束后执行 SafepointSynchronize::end() 时唤醒
Safepoint_lock->wait(Mutex::_no_safepoint_check_flag);
}
// Safepoint 结束,VM Thread 已经唤醒了我们
// 恢复线程原本想要切换去的那个目标状态(例如 _thread_in_vm),继续执行业务
thread->set_thread_state(state);
}
4. 核心流程总结
配合 OpenJDK 8 的源码架构,一个 Blocked 线程在 Safepoint 期间的生命周期遵循以下轨迹:
[线程因某些原因阻塞]
│
▼
线程状态变为 _thread_blocked
│
▼
【VM Thread 发起 Safepoint】 ───> 扫描到该线程 ───> 命中 `safepoint_safe` ───> 视为“已在安全点” (直接略过)
│
▼
【Blocked 线程被客观唤醒】
│
▼
执行状态转换宏 (如 ~ThreadBlockInVM)
│
▼
检测到 `SafepointSynchronize::do_call_back() == true`
│
▼
进入 `SafepointSynchronize::block()`
│
▼
状态保持 `_thread_blocked` ───> 阻塞在 `Safepoint_lock` ───> [真正的物理等待]
│
▼
【VM Thread 完成工作,撤销 Safepoint】
│
▼
线程被唤醒 ───> 恢复原状态 ───> 继续执行

通过这种免视等待(Optimistic Pass)和出口拦截(Pessimistic Check)的非对称设计,HotSpot 完美避免了 VM Thread 去逐一唤醒或等待底层系统底层阻塞线程的性能开销,从而保证了进入 Safepoint 的整体高效率。

687

被折叠的 条评论
为什么被折叠?



