前言

本文旨在记录近期研读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.cpp
hotspot/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.cpp
hotspot/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
         |                            |                           |                            | 陷入条件变量等待 (挂起)

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐