揭秘Java世界中safepoint之核心机制解析概述
核心架构与机制概述
前言
本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限,文中内容难免存在疏漏,恳请读者不吝指正。
Safepoint核心架构与机制概述
Java 虚拟机(JVM)中的 Safepoint(安全点)是一种使所有应用程序线程(Java 线程)在特定位置暂停的机制。这是为了让 JVM 的管理线程(VM Thread)能够安全地执行某些全局性操作(VM Operations),而不用担心 Java 线程同时修改堆内存或寄存器状态。
常见的 VM Operations 触发场景
- 垃圾回收(GC):标记-复制、标记-清除或并发标记的根链扫描阶段,需要绝对静止的引用关系。
- 偏向锁撤销(Biased Lock Revocation):需要检查并修改锁对象的 Mark Word。
- 代码反优化(Deoptimization):将即时编译(JIT)的本地代码转换回解释器执行,需要修改栈帧。
- 线程 Dump:获取所有线程的当前调用栈。
- 各种 Debug/JVMTI 操作:如动态类重定义(Redefine Classes)。
核心原理一:全局内存页主动轮询(Polling Page Mechanism)
在 OpenJDK 8 中,HotSpot 主要采用基于硬件信号的主动轮询机制(Hardware Polling)。为了让高并发下的 Java 线程尽可能快且低消耗地感知到 Safepoint 的到来,JVM 并没有使用全局状态锁,而是将一个特定的内存页(Polling Page)权限设为不可读/不可写。
1. JIT 编译代码中的 Polling 注入
JIT 编译器(C1/C2)在生成机器码时,会在特定的位置插入轮询指令:
- 循环回边(Loop Backedge):防止长循环长时间不进入 Safepoint(非 Counted Loop)。
- 方法返回处(Method Return):在退出方法栈帧前检查。
在 x86 架构下,生成的机器指令通常形如:
test %eax, 0x16000000 ; 0x16000000 是全局 Polling Page 的内存地址
- 正常运行状态:该内存页具有读权限,
test指令是一条极快的空操作(仅修改 CPU 标志位),耗时几乎为零。 - 触发 Safepoint 状态:VM Thread 通过
mprotect系统调用将该页权限改为PROT_NONE(无权限)。当 Java 线程再次执行到test指令时,将触发系统的 OSSIGSEGV(段错误)信号。
2. 解释器中的轮询
Template Interpreter(模板解释器)在执行字节码时,会在字节码派发表(Dispatch Table)或特定的安全点检查点检查全局变量 SafepointSynchronize::_state。
核心原理二:线程状态转移与安全点判定
JVM 将线程划分为不同的状态,并非所有状态的线程都需要物理暂停。
| 线程当前状态 | 对 Safepoint 的响应逻辑 | 安全性判定 |
|---|---|---|
_thread_in_Java |
正在执行 Java 字节码或 JIT 机器码。会执行 Polling 指令。 | 不安全。必须等待其命中轮询点并陷入阻塞。 |
_thread_in_native |
正在执行 Native 方法(通过 JNI)。无法直接操作 Java 堆对象。 | 安全。VM Thread 视其已进入 Safepoint。如果 Native 方法试图返回 Java 环境,则会在边界被拦截阻塞。 |
_thread_in_vm |
正在 JVM 运行时内部执行 C++ 代码。 | 特殊处理。在离开 VM 状态或检查点时,会主动检查 Safepoint 状态并阻塞。 |
_thread_blocked |
线程处于阻塞状态(如等待锁、条件变量、或 Thread.sleep)。 |
安全。VM Thread 视其已进入 Safepoint。在解除阻塞时会检查 Safepoint。 |
OpenJDK 8核心源码深度剖析
以下基于 OpenJDK 8源码,详解 Safepoint 的进入、等待与退出机制。
1. 触发安全点:SafepointSynchronize::begin()
该方法由 VMThread 执行,是进入 STW(Stop-The-World)的核心控制逻辑。
// 源码路径:hotspot/src/share/vm/runtime/safepoint.cpp
void SafepointSynchronize::begin() {
assert(Thread::current()->is_VM_thread(), "Only VM thread can execute this");
// 1. 增加全局安全点计数器,用于追踪
_safepoint_counter++;
// 2. 变更当前的 Safepoint 状态为 _synchronizing(正在同步中)
_state = _synchronizing;
OrderAccess::fence(); // 保证可见性内存屏障
// 3. 关键步骤:激活内存页轮询机制(Arming the polling page)
// 核心原理:通过将 Polling Page 的内存权限设置为不可读写(PROT_NONE),
// 使得 JIT 代码中执行 test 指令时触发硬件级内存保护异常(SIGSEGV)。
if (UseCompilerSafepoints && DeferPollingPageLoopCount < 0) {
guarantee (PageArmed == 0, "invariant");
PageArmed = 1;
// os::protect_memory 是封装了系统的 mprotect
os::protect_memory((char *)os::get_polling_page(), os::vm_page_size(), os::MEM_PROT_NONE);
}
// 4. 解释器安全点激活
// 改变解释器的派发表或者轮询标记,使得解释执行的线程在下一条字节码边界进入安全点机制
Interpreter::notice_safepoints();
// 5. 循环等待所有非安全状态的 Java 线程进入 Safepoint
int iterations = 0;
int cur_sequence = _safepoint_counter;
// 核心等待循环:遍历系统中所有的 JavaThread
for (JavaThread *cur = Threads::first(); cur != NULL; cur = cur->next()) {
assert(cur->is_Java_thread(), "must be a Java thread");
// 检查并更新当前线程的安全点状态
// 如果线程处于 _thread_in_native 等安全状态,VM Thread 会直接将其计入“已到达安全点”
// 如果线程处于 _thread_in_Java,则需要等待其命中轮询点
if (!inline_check_state(cur, cur_sequence)) {
// 如果线程未到达,VM Thread 可能会进行短暂的自旋或睡眠等待
}
}
// 6. 当所有线程都进入安全状态后,变更状态为 _synchronized
_state = _synchronized;
OrderAccess::fence();
// 至此,整个 Java 世界完全静止,VM 操作可以安全执行
}
2. 硬件异常信号捕获:JVM_handle_linux_signal
当 JIT 编译的 Java 线程执行到 test 指令,由于 os::protect_memory 已将页面设为 PROT_NONE,CPU 会抛出异常,由操作系统的信号处理器捕获。
// 源码路径:hotspot/src/os_cpu/linux_x86/vm/os_linux_x86.cpp
// Linux 下的信号处理函数
int JVM_handle_linux_signal(int sig, siginfo_t* info, void* ucVoid, int abort_if_unrecognized) {
address pc = NULL;
ucontext_t* uc = (ucontext_t*)ucVoid;
if (uc != NULL) {
// 获取发生异常时的程序计数器(PC),即发生段错误的 test 指令位置
pc = os::Linux::ucontext_get_Pc(uc);
}
// 1. 判断是否为 SIGSEGV(段错误)且当前 JVM 正在进行安全点同步
if (sig == SIGSEGV && SafepointSynchronize::is_synchronizing()) {
address addr = (address) info->si_addr; // 获取引发异常的内存地址
// 2. 验证该地址是否确实是 JVM 设立的全局 Polling Page 地址
if (os::is_poll_address(addr)) {
// 3. 关键点:将捕获信号后的执行上下文 PC 寄存器,修改为指定的 Safepoint Handler 地址。
// 当信号处理函数返回时,线程不会回到原 test 指令,而是直接跳转到
// os::get_get_safepoint_stub() 对应的汇编存根代码执行
uc->uc_mcontext.gregs[REG_RIP] = (greg_t)os::get_safepoint_stub();
return 1; // 代表该信号已被 JVM 成功处理
}
}
// ... 其他信号处理逻辑 ...
}
os::get_safepoint_stub() 最终会引导线程调用 SafepointSynchronize::block(JavaThread *thread),使其在底层的条件变量上挂起。
3. 线程在安全点阻塞:SafepointSynchronize::block()
当解释器检测到安全点、或者 Native 线程试图返回 Java 代码、或者硬件信号重定向后,都会调用该方法进行真正物理上的阻塞。
// 源码路径:hotspot/src/share/vm/runtime/safepoint.cpp
void SafepointSynchronize::block(JavaThread *thread) {
assert(thread != NULL, "thread must be set");
assert(thread->is_Java_thread(), "not a Java thread");
// 获取当前安全点的代数
int cur_sequence = _safepoint_counter;
// 1. 检查线程状态,如果是从 Native 切换回来,或者正在运行
// 必须在此处将线程的实际状态设置为 _thread_blocked 挂起状态
JavaThreadState state = thread->thread_state();
// 2. 变更线程状态,利用固定的操作原子性,通知 VM Thread 该线程已安全
thread->set_thread_state(_thread_blocked);
// 3. 进入底层锁与条件变量等待循环
// 使用了 Monitor/Mutex 机制(JVM 内部的封装),让当前线程在 Threads_lock 上等待
if (is_synchronizing()) {
// 告诉 VM Thread:我(当前线程)已经安全到达阻塞点
decremented_waiting_to_block();
while (_state != _not_synchronized) {
// 如果安全点没有解除(即仍处于 _synchronizing 或 _synchronized 状态),
// 线程在这里无限期挂起(Wait)
Safepoint_lock->wait(Mutex::_no_safepoint_check_flag);
}
}
// 4. 唤醒后的恢复:当 VM 操作结束,SafepointSynchronize::end() 会将其唤醒
// 线程重新将状态恢复为原先的 Java 或 VM 状态,继续执行原有业务逻辑
thread->set_thread_state(state);
}
4. 恢复执行:SafepointSynchronize::end()
VM 操作完成后,VM Thread 调用此方法,恢复整个 Java 世界的运转。
// 源码路径:hotspot/src/share/vm/runtime/safepoint.cpp
void SafepointSynchronize::end() {
assert(Thread::current()->is_VM_thread(), "Only VM thread can execute this");
// 1. 将状态重新变更为 _not_synchronized(未同步状态)
_state = _not_synchronized;
OrderAccess::fence();
// 2. 关键步骤:恢复内存页轮询机制(Disarming the polling page)
// 将 Polling Page 的权限重新修改为可读(MEM_PROT_READ)
if (UseCompilerSafepoints && PageArmed == 1) {
PageArmed = 0;
os::protect_memory((char *)os::get_polling_page(), os::vm_page_size(), os::MEM_PROT_READ);
}
// 3. 通知解释器恢复正常执行
Interpreter::ignore_safepoints();
// 4. 唤醒所有在 Safepoint_lock 上挂起的业务线程
MutexLockerEx mu(Safepoint_lock, Mutex::_no_safepoint_check_flag);
Safepoint_lock->notify_all();
// 至此,所有的 Java 线程被唤醒,退出 block 状态,继续并发执行
}
性能损耗与开销深度分析
尽管基于 Hardware Polling 的 Safepoint 实现了极高的常态运行效率,但它也是引发 Java 应用长暂停(STW)的主要诱因之一。其性能损耗主要包含两个核心指标:TTSP(Time To Safepoint) 与 Cleanup 损耗。
1. TTSP (Time To Safepoint) 开销
TTSP 是指从 VM Thread 发起安全点请求,到所有 Java 线程完全进入安全点停止下来所消耗的时间。
- 大循环导致 TTSP 过高:如果一个线程正在执行一个昂贵的、包含数十万次循环的方法(例如
for(int i=0; i<1000000; i++)),且该循环是 Counted Loop(可数循环),为了极致的性能,JIT 默认不会在可数循环的回边注入 Polling 指令。这就导致该线程必须等到整个循环全部执行完毕,到达方法返回处才会感知到 Safepoint。在此期间,VM Thread 只能阻塞等待,导致整个 JVM 处于停顿状态。 - 优化参数:可以使用
-XX:+UseCountedLoopSafepoints强制在可数循环中也注入安全点检查。
2. 全局伪共享与 mprotect 系统调用开销
- 当调用
os::protect_memory时,在多核 CPU 架构下,操作系统需要通过 TLB Shootdown(Translation Lookaside Buffer 击落) 向所有 CPU 核心发送中断,同步刷新内存页表权限。在高并发、频繁触发安全点的场景下(如大量的偏向锁撤销),系统调用本身的开销非常明显。 - 所有的 Java 线程都去读取同一个全局 Polling Page,虽然在未触发安全点时是只读的,但一旦发生权限改变,会导致多核 CPU 缓存行(Cache Line)频繁失效。
架构演进注记(关于 JDK 10+ 的 Handshakes 机制)
正是因为 OpenJDK 8 的全局 Safepoint 采用的是“全有或全无”的硬暂停策略(即为了撤销一个线程的偏向锁,不得不暂停全站几百个线程),导致大内存、高并发下的毛刺点无法根治。从 JDK 10 开始,HotSpot 引入了 Thread-Local Handshakes(线程本地握手) 机制,允许 VM Thread 单独针对某个特定线程下达局部 Polling 指令,而无需触发全局 STW。但在生产环境依然广泛使用的 OpenJDK 8 中,上述基于全局内存保护页的代码依然是维系 JVM 稳定运行的最底层基石。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)