揭秘Java世界中safepoint机制之线程进入safepoint机制解析一
本文分析了Java线程进入Safepoint的四种状态机制:JIT编译码通过硬件陷阱轮询指令触发SIGSEGV信号;解释执行通过替换派发表实现;Native代码在返回边界检查;阻塞线程在唤醒时检查。重点剖析了JIT编译码的三阶段机制:编译期插入testl轮询指令,触发期修改内存页权限引发页错误,捕获期通过信号处理器重定向执行流。该机制利用操作系统内存保护和信号处理实现高效异步轮询,避免频繁检查全局
线程进入Safepoint机制
前言
本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限,文中内容难免存在疏漏,恳请读者不吝指正。
线程进入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 视为默认安全。它们在被唤醒或者获取到锁准备恢复运行的瞬间,也会进行边界检查并阻塞。 |
处于Compiled(JIT编译码)线程进入safepoint机制解析
在 OpenJDK 8 的 HotSpot 虚拟机中,Compiled Code(JIT 编译码) 状态下的线程进入 Safepoint 采用的是一种极其高效的基于硬件陷阱的异步轮询机制(Implicit / Hardware-based Polling)。
与解释执行逐条解析字节码不同,JIT 编译后的代码直接以机器码在 CPU 上原生运行。为了不破坏底层流水线的执行效率,HotSpot 并没有在机器码中频繁插入“判断全局变量是否为真”的传统分支跳转(if(safepoint_requested) wait();),而是利用操作系统的虚拟内存保护机制(MMU)与信号量处理机制。
阶段一:JIT 编译器插入轮询指令(编译期)
在代码编译阶段,C1 或 C2 编译器会在方法返回处(Return)以及循环的回边(Loop Backedge)隐式插入一条特殊的测试指令——Safepoint Poll。
源码路径
hotspot/src/cpu/x86/vm/c1_LIRAssembler_x86.cpp(以 C1 编译器为例)hotspot/src/cpu/x86/vm/macroAssembler_x86.cpp
源码实现与深度注释
// 在方法返回(Return)时,C1 编译器生成安全点检测代码
void LIR_Assembler::return_op(LIR_Opr result) {
// ... 省略栈帧弹出等常规操作
// 核心步骤:向代码缓冲区中织入一个安全点重定位标记(Relocation Mark)
// 这表明接下来的指令是一个用于安全点检测的轮询操作
__ relocate(relocInfo::poll_return_type);
// 生成本地机器指令:testl %eax, (polling_page_address)
// 1. 在正常情况下,os::get_polling_page() 对应的内存页是【可读】的。
// 该 testl 指令仅仅是做一次普通的内存读,CPU 会将其视为无害的空操作(NOP-like),耗时接近 0。
// 2. 当全局安全点激活时,该内存页会被改为【不可读】(PROT_NONE),此处将立刻触发硬件页错误(Page Fault)。
__ testl(rax, Address(nmethod::polling_page_address(), 0));
// 真正执行机器层的返回指令
__ ret(0);
}
阶段二:VM Thread 激活安全点(触发期)
当虚拟机由于 GC 或其他原因需要进入 Safepoint 时,负责协调的 VM Thread 会首先运行,将原本所有人都可以正常读取的全局轮询页面(Polling Page)标记为不可读。
源码路径
hotspot/src/share/vm/runtime/safepoint.cpphotspot/src/os/linux/vm/os_linux.cpp
源码实现与深度注释
void SafepointSynchronize::begin() {
// ... 省略前置状态检查和锁竞争逻辑
// 关键标志位:设置当前虚拟机的安全点状态为 _synchronizing(正在同步中)
_state = _synchronizing;
// 强制刷新所有 CPU 核心的缓存,确保多核可见性
OrderAccess::fence();
if (UseCompilerSafepoints) {
// 核心操作:将全局变量 PageArmed 标记为 1,代表轮询页已经设防
guarantee (PageArmed == 0, "invariant");
PageArmed = 1;
// 调用平台相关的底层函数,将安全点轮询页面设置为不可读
os::make_polling_page_unreadable();
}
// ... VM Thread 进入循环等待,直到所有 Compiled/Interpreter 线程向其报到
}
// Linux 平台下的具体内存保护实现
void os::make_polling_page_unreadable(void) {
// 通过底层系统调用 mprotect,将虚拟地址 _polling_page 对应的内存页权限变更为 PROT_NONE(无权限)
// 此时,任何线程一旦试图读取(如执行上述 testl 指令)或写入该页面,
// 操作系统底层的 MMU(内存管理单元)就会发出一个硬件层面的页保护异常。
if (!linux_mprotect((char *)_polling_page, Linux::page_size(), PROT_NONE)) {
fatal("Could not disable polling page");
}
}
阶段三:操作系统信号拦截与上下文重定向(捕获期)
当处于 Compiled 状态的 Java 线程执行到 testl 指令时,由于 MMU 拦截,操作系统会向该线程投递一个 SIGSEGV(段错误) 信号。JVM 在启动时就已经注册了自定义信号处理器,从而在此时接管控制权。
源码路径
hotspot/src/os_cpu/linux_x86/vm/os_linux_x86.cpp
源码实现与深度注释
extern "C" JNIEXPORT int JVM_handle_linux_signal(int sig, siginfo_t* info,
void* ucVoid, int abort_if_unrecognized) {
// 将原生的 ucontext 转换为 Linux 平台的特定结构
ucontext_t* uc = (ucontext_t*)ucVoid;
// 获取触发当前信号时的 CPU 寄存器上下文
address pc = (address)os::Linux::ucontext_get_pc(uc);
// 核心校验:判断引发 SIGSEGV 的错误内存地址是否正是 JVM 的 Safepoint Polling Page
if (sig == SIGSEGV && os::is_poll_address((address)info->si_addr)) {
// 获取一段平台相关的运行时安全点跳转桩代码(SafepointBlob 的 Entry)
address stub = SharedRuntime::get_poll_stub(pc);
// 【高能操作】:直接改写当前被中断线程的上下文寄存器!
// 将其下一次准备执行的指令指针(PC / RIP 寄存器)强行修改为 stub 桩代码的入口地址。
os::Linux::ucontext_set_pc(uc, stub);
// 返回 1 告诉操作系统:该信号已被 JVM 成功处理,无需崩溃退出。
// 当操作系统恢复线程执行时,线程将不会回到原来的 testl 后面继续执行,
// 而是直接跳转到我们修改后的 stub 代码中。
return 1;
}
// ... 其他信号(如无害的 NullPointerException 检查、StackOverflow 检查等)
}
阶段四:进入运行时桩代码并陷入阻塞(阻塞期)
通过改写寄存器指针,线程在恢复执行后直接跃迁至 SafepointBlob。这里负责保存当前全部线程现场(寄存器压栈),并将线程状态变更为过度状态,最终由 SafepointSynchronize::block 真正将其挂起。
源码路径
hotspot/src/share/vm/runtime/sharedRuntime.cpphotspot/src/share/vm/runtime/safepoint.cpp
源码实现与深度注释
// 平台独立的运行时桩代码生成
void SharedRuntime::generate_handler_blob() {
// 该 Blob 内部会通过汇编生成以下逻辑:
// 1. PUSHALL:将所有通用寄存器(RAX, RBX, RCX...)以及 XMM 寄存器压入线程栈保存,保护现场。
// 2. 调用 C++ 运行时函数:SafepointSynchronize::block(JavaThread* thread)
}
// 最终的阻塞核心逻辑
void SafepointSynchronize::block(JavaThread *thread) {
// 1. 获取线程当前真实状态并记录
JavaThreadState state = thread->thread_state();
// 2. 状态跃迁变更:将线程状态由原先的 _thread_in_Java 改为 _thread_in_Java_trans(处于安全点过渡状态)
thread->set_thread_state(_thread_in_Java_trans);
OrderAccess::fence(); // 保证状态立即可见
// 3. 检查安全点同步是否尚未结束
if (SafepointSynchronize::do_call_back()) {
// 4. 将自己真正标记为阻塞状态,向 VM Thread 报告:我已经安全停下
thread->set_thread_state(_thread_blocked);
// 5. 线程在底层条件变量或信号量上陷入自旋或挂起,
// 等待 VM Thread 完成操作并调用 SafepointSynchronize::end() 来将其唤醒
SafepointSynchronize::wait_for_safepoint_notification();
}
// 6. 安全点解除后,恢复原先的线程状态,继续快乐地执行编译码
thread->set_thread_state(state);
}
核心流程时序总结
通过这一整套精密的设计,HotSpot 完美地在 Compiled 状态下实现了“零成本轮询”:
+------------------+ +------------------+ +----------------+ +----------------------+
| Java编译码线程 | | 操作系统内核 | | JVM 信号处理器 | | Runtime 安全点处理桩 |
+------------------+ +------------------+ +----------------+ +----------------------+
| | | |
执行 testl 机器码 | | |
-------->| | | |
| (此时页面不可读) | | |
|===========================>| | |
| 触发硬件 Page Fault | | |
| |----> 投递 SIGSEGV | |
| |-------------------------->| |
| | | |
| | | 改写 ucontext 的 PC |
| | | 寄存器指向 Stub 入口 |
| | |------------+ |
| | |<-----------+ |
| |<--------------------------| |
| | 信号处理返回并恢复线程 | |
|<---------------------------| | |
| | | |
跳转至 Stub 代码执行 | | |
============================================================================================-->|
| | | | 保存所有寄存器现场
| | | | 状态变更为 _thread_blocked
| | | | 陷入条件变量等待 (挂起)
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)