前言

本文旨在记录近期研读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 指令时,将触发系统的 OS SIGSEGV(段错误)信号

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 稳定运行的最底层基石。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐