深入剖析 OpenJDK 17 解释器中的安全点(Safepoint)进入与退出机制

1. 引言

Java 虚拟机(JVM)中的安全点(Safepoint)是一种让所有 Java 线程同时暂停执行的特殊时刻,常用于执行全局性操作,如垃圾回收(GC)的全局标记、代码去优化(deoptimization)、偏向锁撤销等。理解安全点的实现对于掌握 JVM 的运行时行为、性能调优以及调试疑难问题都至关重要。

在 HotSpot 虚拟机中,Java 代码的执行方式主要分为解释执行和编译执行(JIT 编译后的机器码)。虽然编译代码中的安全点实现通常更为人熟知,但解释器中的安全点机制同样重要且独特------它通过精心设计的入口点(entry points)和分发表(dispatch table)来实现低开销的轮询。本文将以 OpenJDK 17 源码为基础,完整梳理解释器从识别安全点请求、进入 VM 线程、阻塞等待安全点,到安全点结束后恢复执行的全过程。

我们将重点分析以下源码模块:

  • TemplateInterpreterGenerator::generate_safept_entry_for 如何生成安全点专用的 entry point;

  • InterpreterRuntime::at_safepoint 作为安全点回调的入口;

  • JRT_ENTRY 宏及其背后的 ThreadInVMfromJava 状态转换类;

  • SafepointMechanism::process_if_requestedSafepointSynchronize::block 如何让线程真正阻塞;

  • dispatch_base 中的轮询指令如何检测安全点请求并跳转到安全点入口。

通过逐段剖析源码,我们将揭示一条清晰的逻辑链条:解释器执行字节码 → 每轮调度时检查轮询页(polling page)→ 发现安全点请求 → 调用 InterpreterRuntime::at_safepoint → 状态转换(_thread_in_Java_thread_in_vm)→ 处理安全点 → 阻塞直到安全点结束 → 返回解释器继续执行。

2. 解释器安全点入口点的生成

2.1 EntryPoint 结构

在 HotSpot 解释器中,每个字节码的入口点(entry point)根据栈顶状态(TosState)分为不同的版本:itos(int)、ltos(long)、ftos(float)、dtos(double)、atos(对象)、vtos(void 或任意类型)。EntryPoint 类封装了这六种状态的入口地址。

安全点机制需要为每个 TosState 也提供一个专门的入口地址。当解释器检测到当前线程需要进入安全点时,它会放弃正常的字节码分发表,转而使用安全点分发表(safept_table),跳转到对应的 _safept_entry

2.2 generate_safept_entry_for 的实现

下面的代码来自 templateInterpreterGenerator.cpp(OpenJDK 17 源码):

cpp

复制代码
{ CodeletMark cm(_masm, "safepoint entry points");
  Interpreter::_safept_entry =
    EntryPoint(
      generate_safept_entry_for(atos, CAST_FROM_FN_PTR(address, InterpreterRuntime::at_safepoint)),
      generate_safept_entry_for(itos, CAST_FROM_FN_PTR(address, InterpreterRuntime::at_safepoint)),
      generate_safept_entry_for(ltos, CAST_FROM_FN_PTR(address, InterpreterRuntime::at_safepoint)),
      generate_safept_entry_for(ftos, CAST_FROM_FN_PTR(address, InterpreterRuntime::at_safepoint)),
      generate_safept_entry_for(dtos, CAST_FROM_FN_PTR(address, InterpreterRuntime::at_safepoint)),
      generate_safept_entry_for(vtos, CAST_FROM_FN_PTR(address, InterpreterRuntime::at_safepoint))
    );
}

generate_safept_entry_for 函数生成一小段汇编代码:

cpp

复制代码
address TemplateInterpreterGenerator::generate_safept_entry_for(
        TosState state,
        address runtime_entry) {
  address entry = __ pc();
  __ push(state);
  __ call_VM(noreg, runtime_entry);
  __ dispatch_via(vtos, Interpreter::_normal_table.table_for(vtos));
  return entry;
}

这段汇编的核心动作:

  1. 保存当前栈顶状态push(state) 实际上根据不同的 TosState 将当前的 rax 等寄存器的值压栈,保证在调用 VM 函数时参数完整。

  2. 调用 VM 运行时函数call_VM 宏会生成代码保存现场、设置 JavaFrameAnchor,并调用 InterpreterRuntime::at_safepoint。注意第一个参数是 noreg,表示该函数没有返回值。

  3. 恢复执行 :安全点处理完成后,通过 dispatch_via(vtos, Interpreter::_normal_table.table_for(vtos)) 重新进入正常的字节码分发表。这意味着安全点回来后,解释器会以 vtos(无栈顶值)状态开始调度下一条字节码------因为之前的安全点调用已经消耗了当前字节码的栈顶值(例如一条加法字节码在执行前遇到安全点,参数已压栈,安全点返回后这些参数可能已失效,所以直接以 vtos 开始是安全的)。

2.3 安全点分发表与正常分发表的关系

解释器的正常分发表 _normal_table 为每个字节码提供了直接执行的入口;而安全点分发表 _safept_table 中的所有入口都指向上述 generate_safept_entry_for 生成的代码。在解释器执行每条字节码之前(或之后,取决于具体的调度位置),会检查线程本地的一个轮询标志(polling bit),如果该标志被置位,则跳转到安全点分发表中的对应入口,否则继续使用正常分发表。

这个检查逻辑位于 InterpreterMacroAssembler::dispatch_base 中,我们将在第 7 节详细分析。

3. 安全点处理的核心函数:InterpreterRuntime::at_safepoint

当解释器跳转到安全点入口后,最终会调用 InterpreterRuntime::at_safepoint。源码如下:

cpp

复制代码
JRT_ENTRY(void, InterpreterRuntime::at_safepoint(JavaThread* current))
  // 之前需要显式保存参数的代码已不再需要,因为栈遍历会自动处理 invoke 字节码的参数。

  // JRT_END 隐含了安全点检查,因此如果我们在安全点期间被调用,一定会阻塞。

  if (JvmtiExport::should_post_single_step()) {
    // 当单步调试时,该函数会被解释器调用。单步可能会 unwind 栈帧,
    // 因此需要确保处理返回过程中可能遇到的帧。
    StackWatermarkSet::before_unwind(current);

    // 我们可能在常规安全点或单步时被调用。如果任意线程标记了单步,
    // 则需要执行 JVMTI 的工作。
    LastFrameAccessor last_frame(current);
    JvmtiExport::at_single_stepping_point(current, last_frame.method(), last_frame.bcp());
  }
JRT_END

这个函数本身看起来非常简短,主要处理 JVMTI 单步调试的回调。真正的安全点阻塞逻辑并不直接写在这里,而是通过 JRT_ENTRYJRT_END 宏间接实现。关键点在于 JRT_END 宏会自动生成安全点检查和可能的阻塞代码。因此,理解 JRT_ENTRY 宏背后的状态转换机制至关重要。

4. JRT_ENTRY 宏与 Java ↔ VM 状态转换

4.1 JRT_ENTRY 宏展开分析

JRT_ENTRY 是 HotSpot 中用于定义从解释器或 JIT 代码调用的运行时函数的宏。其简化定义如下:

cpp

复制代码
#define JRT_ENTRY(result_type, header)                               \
  result_type header {                                               \
    MACOS_AARCH64_ONLY(ThreadWXEnable __wx(WXWrite, current));       \
    ThreadInVMfromJava __tiv(current);                               \
    VM_ENTRY_BASE(result_type, header, current)                      \
    debug_only(VMEntryWrapper __vew;)

结合我们的调用 JRT_ENTRY(void, InterpreterRuntime::at_safepoint(JavaThread* current)),展开后大致为:

cpp

复制代码
void InterpreterRuntime::at_safepoint(JavaThread* current) {
  ThreadWXEnable __wx(WXWrite, current);   // macOS/AArch64 特定,用于内存写权限
  ThreadInVMfromJava __tiv(current);
  // ... 后续可能有的 VM_ENTRY_BASE 和 debug 代码
  // 函数体
  if (JvmtiExport::should_post_single_step()) { ... }
  // 函数体结束
}

4.2 ThreadInVMfromJava:状态转换的 RAII 类

ThreadInVMfromJava 是一个 RAII(资源获取即初始化)类,在其构造函数中将线程状态从 _thread_in_Java 转换为 _thread_in_vm,在其析构函数中再将状态切回 _thread_in_Java。其核心实现:

cpp

复制代码
class ThreadInVMfromJava : public ThreadStateTransition {
  bool _check_asyncs;
public:
  ThreadInVMfromJava(JavaThread* thread, bool check_asyncs = true) 
    : ThreadStateTransition(thread), _check_asyncs(check_asyncs) {
    trans_from_java(_thread_in_vm);
  }
  ~ThreadInVMfromJava() {
    if (_thread->stack_overflow_state()->stack_yellow_reserved_zone_disabled()) {
      _thread->stack_overflow_state()->enable_stack_yellow_reserved_zone();
    }
    trans(_thread_in_vm, _thread_in_Java);
    if (_thread->has_special_runtime_exit_condition()) 
      _thread->handle_special_runtime_exit_condition(_check_asyncs);
  }
};

构造函数调用了 trans_from_java(_thread_in_vm),而 trans_from_java 是一个模板方法,最终调用静态函数 transition

cpp

复制代码
static inline void transition(JavaThread *thread, JavaThreadState from, JavaThreadState to) {
  assert(from != _thread_in_Java, "use transition_from_java");
  assert(from != _thread_in_native, "use transition_from_native");
  assert((from & 1) == 0 && (to & 1) == 0, "odd numbers are transitions states");
  assert(thread->thread_state() == from, "coming from wrong thread state");

  // 检查 NoSafepointVerifier(如果有的话),并清空 unhandled oops
  thread->check_possible_safepoint();

  // 先改变到中间状态(_thread_in_vm_trans),加内存屏障确保可见性
  thread->set_thread_state_fence((JavaThreadState)(from + 1));

  // 处理可能的安全点请求
  SafepointMechanism::process_if_requested(thread);

  // 最终设置为目标状态
  thread->set_thread_state(to);
}

注意 trans_from_java 专门用于从 _thread_in_Java 出发的转换,其 from 参数隐含为 _thread_in_Java,因此第一步会将状态设为 _thread_in_Java_trans(因为 from+1 等于 _thread_in_Java_trans)。然后调用 SafepointMechanism::process_if_requested,这正是安全点检查和处理的核心。最后才将状态变为 _thread_in_vm

同理,析构函数中的 trans(_thread_in_vm, _thread_in_Java) 会先变为 _thread_in_vm_trans,再次调用 process_if_requested(确保从 VM 返回时也检查安全点),最后变为 _thread_in_Java,并检查异步异常。

4.3 为什么需要中间状态(_thread_in_Java_trans)?

HotSpot 中的线程状态不仅包括 _thread_in_Java_thread_in_vm,还包括一系列"过渡"状态(奇数编号):_thread_in_Java_trans_thread_in_vm_trans_thread_in_native_trans 等。这些过渡状态的作用是让 VM 线程(执行安全点的线程)能够精确感知其他线程正在转换状态,从而避免安全点死锁或遗漏。

当 VM 线程发起全局安全点时,它会遍历所有 Java 线程,检查每个线程的状态:

  • 如果状态是 _thread_in_Java,表示线程正在执行 Java 代码,VM 线程会等待它下次轮询时进入安全点。

  • 如果状态是 _thread_in_Java_trans,表示该线程正在从 Java 转换到 VM 的途中,VM 线程会自旋等待直到它变为 _thread_in_vm,因为一旦进入 VM,该线程通常会快速到达安全点检查点。

  • 如果状态是 _thread_in_vm,说明线程已经在 VM 中,必须等待它主动调用安全点阻塞(或完成关键操作后自行阻塞)。

通过这种设计,VM 线程可以安全地等待所有线程到达安全点,而不会丢失正在转换中的线程。

4.4 process_if_requested 的调用时机

transition 函数中,SafepointMechanism::process_if_requested 是在设置为过渡状态之后、目标状态之前调用的。这样做的目的是:如果一个线程从 Java 进入 VM 的过程中,VM 已经发起了全局安全点,那么该线程会在这里立即检测到并主动阻塞,而不是等到进入 VM 后再被动的被 VM 线程挂起。这种"主动式"安全点大大提高了响应速度。

5. SafepointMechanism:统一的安全点轮询机制

5.1 本地轮询与全局轮询

SafepointMechanism::process_if_requested 的实现:

cpp

复制代码
void SafepointMechanism::process_if_requested(JavaThread* thread, bool allow_suspend) {
#if defined(ASSERT) && defined(__APPLE__) && defined(AARCH64)
  if (AssertWXAtThreadSync) {
    thread->assert_wx_state(WXWrite);
  }
#endif

  if (local_poll_armed(thread)) {
    process(thread, allow_suspend);
  }
}

local_poll_armed(thread) 检查线程本地的轮询字(polling word)是否被置位。在 HotSpot 中,每个 JavaThread 对象都有一个 _polling_word 字段,VM 线程通过将该字段设置为特定值来通知该线程需要进行安全点处理。local_poll_armed 通常只是检查一个位(SafepointMechanism::poll_bit())。

如果本地轮询被置位,则调用 process

cpp

复制代码
void SafepointMechanism::process(JavaThread *thread, bool allow_suspend) {
  // 确保读取全局轮询和 handshake 状态不会重排到本地轮询之前
  OrderAccess::loadload();

  bool need_rechecking;
  do {
    if (global_poll()) {
      // 任何在 block() 中的加载都不能越过全局轮询的加载
      OrderAccess::loadload();
      SafepointSynchronize::block(thread);
    }

    // 处理栈水印(StackWatermark),用于栈遍历和 OopMap 更新
    StackWatermarkSet::on_safepoint(thread);

    // 检查是否有 handshake 操作需要当前线程自行处理
    need_rechecking = thread->handshake_state()->has_operation() && 
                      thread->handshake_state()->process_by_self(allow_suspend);
  } while (need_rechecking);

  // 更新轮询值,例如重新加载全局安全点计数器
  update_poll_values(thread);
  OrderAccess::cross_modify_fence();
}

global_poll() 检查全局安全点是否正在发生。如果是,则调用 SafepointSynchronize::block 让线程真正阻塞,等待安全点结束。need_rechecking 循环处理手握手(Handshake)操作------这是一种轻量级的线程间交互,不一定要全局暂停,但这里也集成在同一个流程中。

5.2 内存屏障的重要性

代码中多次出现 OrderAccess::loadload()OrderAccess::cross_modify_fence(),这些是内存屏障,用于防止 CPU 或编译器重排指令顺序,确保线程在决定阻塞之前确实看到了最新的全局安全点状态。在多处理器系统中,这些屏障对于正确实现安全点语义至关重要。

6. SafepointSynchronize::block:线程如何真正阻塞

process 确认全局安全点正在发生时,会调用 SafepointSynchronize::block(thread)。这是安全点实现中最关键的函数之一:

cpp

复制代码
void SafepointSynchronize::block(JavaThread *thread) {
  assert(thread != NULL, "thread must be set");

  // 避免在线程打印时阻塞(例如 tty 锁)
  ttyLocker::break_tty_lock_for_safepoint(os::current_thread_id());

  // 如果线程已经终止(正在退出),则特殊处理
  if (thread->is_terminated()) {
    thread->block_if_vm_exited();
    return;
  }

  JavaThreadState state = thread->thread_state();
  thread->frame_anchor()->make_walkable(thread);

  uint64_t safepoint_id = SafepointSynchronize::safepoint_counter();
  
  switch(state) {
    case _thread_in_vm_trans:
    case _thread_in_Java:
    case _thread_in_native_trans:
    case _thread_blocked_trans:
    case _thread_new_trans:
      // 记录当前线程所属的安全点 ID
      thread->safepoint_state()->set_safepoint_id(safepoint_id);

      // 内存屏障,确保上面的 store 不会与后续 store 重排
      OrderAccess::storestore();
      // 将线程状态改为 _thread_blocked,这是阻塞状态的正式标志
      thread->set_thread_state_fence(_thread_blocked);

      // 等待全局安全点 barrier
      _wait_barrier->wait(static_cast<int>(safepoint_id));
      assert(_state != _synchronized, "Can't be");

      // 从等待中唤醒后,恢复原来的状态
      OrderAccess::loadstore();
      thread->set_thread_state(state);

      // 重置安全点 ID
      thread->safepoint_state()->reset_safepoint_id();
      OrderAccess::fence();
      break;

    default:
      fatal("Illegal threadstate encountered: %d", state);
  }
  // 保证安全点 ID 已重置
  guarantee(thread->safepoint_state()->get_safepoint_id() == InactiveSafepointCounter,
            "The safepoint id should be set only in block path");
}

这个函数的核心是 _wait_barrier->wait(safepoint_id)WaitBarrier 是 HotSpot 中实现高效线程同步的机制,它类似于一个倒计时门闩(CountDownLatch):每个线程在进入安全点时都会调用 wait,并将自己挂起;当所有线程都到达后,VM 线程会执行安全点操作,然后调用 _wait_barrier->disarm() 唤醒所有等待的线程。

6.1 安全点 ID 与状态恢复

注意线程在等待前会保存原来的状态(例如 _thread_in_vm_trans),并在唤醒后恢复该状态。这样,从 block 返回后,process 函数中的 update_poll_values 会重新加载轮询值(通常清除了本地轮询位),然后 cross_modify_fence 确保指令缓存一致性。

接着,process 函数返回,transition 函数继续执行 thread->set_thread_state(to),将状态设置为目标状态(_thread_in_vm),完成从 Java 到 VM 的转换。此时,安全点已经结束,线程可以继续执行 InterpreterRuntime::at_safepoint 的函数体(例如 JVMTI 单步回调),然后最终从 JRT_ENTRY 返回。

7. 解释器中的安全点轮询:dispatch_base 中的细节

解释器执行字节码的典型流程是:从 _bcp_register(字节码指针)读取一个字节码,然后通过分发表跳转到对应的执行例程。在 dispatch_viadispatch_base 中,HotSpot 巧妙地嵌入了安全点轮询。

dispatch_base 的实现(x64 版本,为清晰起见,我们看 #ifdef _LP64 分支):

cpp

复制代码
void InterpreterMacroAssembler::dispatch_base(TosState state,
                                              address* table,
                                              bool verifyoop,
                                              bool generate_poll) {
  // ... 前面省略栈帧验证和 oop 验证代码

  address* const safepoint_table = Interpreter::safept_table(state);
#ifdef _LP64
  Label no_safepoint, dispatch;
  if (table != safepoint_table && generate_poll) {
    NOT_PRODUCT(block_comment("Thread-local Safepoint poll"));
    // 测试线程本地 polling word 的安全点标志位
    testb(Address(r15_thread, JavaThread::polling_word_offset()), SafepointMechanism::poll_bit());
    jccb(Assembler::zero, no_safepoint);

    // 如果安全点位被置位,则使用安全点分发表
    lea(rscratch1, ExternalAddress((address)safepoint_table));
    jmpb(dispatch);
  }

  bind(no_safepoint);
  lea(rscratch1, ExternalAddress((address)table));
  bind(dispatch);
  // 以字节码索引(rbx)为下标,跳转到表中的地址
  jmp(Address(rscratch1, rbx, Address::times_8));
#else
  // 32位版本类似,使用 get_thread(rcx) 获取线程指针
#endif
}

关键点:

  • r15_thread 在 x64 上专门用于存放当前 JavaThread 的指针(类似 fs 段寄存器),这是 HotSpot 的优化。

  • testb 指令读取 JavaThread::polling_word_offset() 处的字节,并与安全点标志位做与测试。如果标志位为 0,说明没有安全点请求,跳转到 no_safepoint,使用正常的字节码分发表;如果非 0,则加载 safepoint_table,随后跳转到安全点入口。

  • 注意安全点分发表中的每个入口都会执行 generate_safept_entry_for 生成的代码,最终调用 InterpreterRuntime::at_safepoint,完成我们前面分析的一系列转换和阻塞。

这个轮询发生在每条字节码的调度时刻,而不是每次指令执行后。因为解释器执行一条字节码可能涉及较多操作(如加法、访问字段),但只有在切换到下一条字节码之前才检查安全点,这样既能保证响应速度,又避免了过高的开销。事实上,这种"在每个字节码边界上轮询"的策略是 HotSpot 解释器的经典设计。

值得一提的是,对于 _thread_in_Java 状态下的线程,安全点轮询是通过内存保护页(guard page)来触发的:VM 线程将 polling page 设置为不可访问(unmapped),当 Java 线程执行到 testb 指令时,由于访问了保护区,会触发 SIGSEGV 信号,信号处理函数随后将线程引导至安全点阻塞。但在我们分析的 dispatch_base 代码中,采用的是主动测试标志位的方式,这是为了性能优化------实际中 HotSpot 会根据配置选择使用信号陷阱还是主动轮询。

8. 完整流程串联:从字节码到安全点返回

现在我们可以将前面各个部分串联成一个完整的执行序列,假设有一个线程正在解释执行 Java 方法,此时 VM 线程发起了一个全局安全点(例如 GC 的 VM_Operation):

  1. VM 线程设置全局标志 :VM 线程将 SafepointSynchronize::_state 设为 _synchronizing,并更新安全点计数器。同时,它遍历所有 Java 线程,对每个线程设置其 _polling_word 的安全点位(或者通过 mprotect 移除 polling page 的访问权限)。

  2. 解释器执行一条字节码并准备调度下一条 :在 dispatch_base 中,解释器执行 testb(Address(r15_thread, ...), poll_bit())。此时由于安全点位已置位,条件跳转失败,解释器转而加载 safepoint_table,然后跳转到安全点入口。

  3. 调用安全点运行时函数 :安全点入口的代码调用 push(state),然后 call_VM 调用 InterpreterRuntime::at_safepoint(current)call_VM 会先设置 JavaFrameAnchor,保存返回地址等。

  4. 状态转换(Java → VM) :进入 JRT_ENTRY 宏,构造 ThreadInVMfromJava 对象。构造函数调用 trans_from_java(_thread_in_vm)transition,将线程状态从 _thread_in_Java 改为 _thread_in_Java_trans,然后调用 SafepointMechanism::process_if_requested

  5. 安全点处理与阻塞process_if_requested 检测到 local_poll_armed 为 true,进入 process 函数。由于 global_poll() 为 true(因为 VM 线程发起了全局安全点),调用 SafepointSynchronize::block(thread)block 将状态改为 _thread_blocked,然后调用 _wait_barrier->wait() 挂起线程。线程此时不再占用 CPU,等待 VM 线程完成安全点操作。

  6. VM 线程执行安全点操作 :当所有 Java 线程都进入了 _thread_blocked 状态后,VM 线程执行安全点内的操作(如垃圾回收),然后设置 _wait_barrierdisarmed 状态,唤醒所有等待的线程。

  7. 线程被唤醒并继续 :被唤醒的线程从 _wait_barrier->wait 返回,恢复原状态(_thread_in_Java_trans),然后返回到 process 函数。接着循环处理 handshake(如果有),然后 update_poll_values 清除本地轮询位,执行 cross_modify_fenceprocess 返回后,transition 将状态最终设置为 _thread_in_vm

  8. 执行函数体并返回 :此时 InterpreterRuntime::at_safepoint 的函数体开始执行(例如 JVMTI 单步回调)。函数体执行完后,JRT_ENTRY 宏的结尾(隐式的 JRT_END)会调用 ThreadInVMfromJava 的析构函数,将状态从 _thread_in_vm 转回 _thread_in_Java(同样经过过渡状态和安全点检查,但此时全局安全点已结束,因此不会再次阻塞)。

  9. 返回解释器继续执行 :安全点入口代码的最后是 dispatch_via(vtos, Interpreter::_normal_table.table_for(vtos)),它会重新进入正常的字节码分发表,开始解释执行下一条字节码(或者同一条字节码的重新调度)。至此,安全点的一次完整进入和退出就结束了。

9. 与编译代码中安全点的对比

编译代码(由 C1 或 C2 生成的机器码)中的安全点通常通过两种方式实现:

  • 显式轮询 :在编译代码的"返回点"或"方法调用前"插入 test 指令,检查 polling page 是否可访问。

  • 隐式安全点:通过栈上替换(OSR)或方法入口的检查。

解释器与编译代码的安全点机制最大的不同在于:

  • 解释器天然在每个字节码边界上都有调度机会,因此可以把轮询集中到 dispatch_base 中,只需一次检查即可覆盖所有字节码。

  • 编译代码由于不能保证有规律的检查点,需要在多个位置插入轮询,开销稍高但更灵活。

另外,状态转换部分(ThreadInVMfromJava)和 SafepointMechanism 是共享的基础设施,无论是解释器还是编译代码,在调用运行时函数时都会经历相同的安全点检查流程。

10. 总结与思考

通过深入分析 OpenJDK 17 的源码,我们完整还原了解释器在安全点进入和退出过程中的每一个关键步骤:

  1. 入口点准备TemplateInterpreterGenerator 为每个 TosState 生成安全点入口,调用 InterpreterRuntime::at_safepoint

  2. 状态转换 RAIIJRT_ENTRY 宏中的 ThreadInVMfromJava 负责 Java ↔ VM 状态的切换,并在转换期间插入安全点检查。

  3. 统一轮询机制SafepointMechanism 通过线程本地 polling word 和全局标志实现轻量级的安全点检测。

  4. 阻塞等待SafepointSynchronize::block 利用 WaitBarrier 高效挂起和唤醒线程。

  5. 解释器轮询点dispatch_base 中的 testb 指令在每个字节码调度时检查安全点请求,并切换到安全点分发表。

整个设计体现了 HotSpot 在并发控制和性能之间的精密平衡:通过状态转换的原子性保证线程安全,通过内存屏障保证多处理器可见性,通过 RAII 和宏封装减少代码重复,通过轮询点位置选择降低开销。理解这些实现细节,对于诊断高并发环境下的"GC 停顿时间长"、"线程卡死"等问题,以及编写低延迟 Java 应用都有极大的帮助。

希望这篇博客能帮助读者建立起对 JVM 安全点机制的直观认识,同时也鼓励大家继续阅读 OpenJDK 源码,探索更多运行时奥秘。安全点只是冰山一角,后面还有锁膨胀、偏向锁、内存模型、编译器优化等更广阔的世界等待我们去发掘。

##源码

cpp 复制代码
{ CodeletMark cm(_masm, "safepoint entry points");
    Interpreter::_safept_entry =
      EntryPoint(
                 generate_safept_entry_for(atos, CAST_FROM_FN_PTR(address, InterpreterRuntime::at_safepoint)),
                 generate_safept_entry_for(itos, CAST_FROM_FN_PTR(address, InterpreterRuntime::at_safepoint)),
                 generate_safept_entry_for(ltos, CAST_FROM_FN_PTR(address, InterpreterRuntime::at_safepoint)),
                 generate_safept_entry_for(ftos, CAST_FROM_FN_PTR(address, InterpreterRuntime::at_safepoint)),
                 generate_safept_entry_for(dtos, CAST_FROM_FN_PTR(address, InterpreterRuntime::at_safepoint)),
                 generate_safept_entry_for(vtos, CAST_FROM_FN_PTR(address, InterpreterRuntime::at_safepoint))
                 );
  }
  
JRT_ENTRY(void, InterpreterRuntime::at_safepoint(JavaThread* current))
  // We used to need an explict preserve_arguments here for invoke bytecodes. However,
  // stack traversal automatically takes care of preserving arguments for invoke, so
  // this is no longer needed.

  // JRT_END does an implicit safepoint check, hence we are guaranteed to block
  // if this is called during a safepoint

  if (JvmtiExport::should_post_single_step()) {
    // This function is called by the interpreter when single stepping. Such single
    // stepping could unwind a frame. Then, it is important that we process any frames
    // that we might return into.
    StackWatermarkSet::before_unwind(current);

    // We are called during regular safepoints and when the VM is
    // single stepping. If any thread is marked for single stepping,
    // then we may have JVMTI work to do.
    LastFrameAccessor last_frame(current);
    JvmtiExport::at_single_stepping_point(current, last_frame.method(), last_frame.bcp());
  }
JRT_END

#define JRT_ENTRY(result_type, header)                               \
  result_type header {                                               \
    MACOS_AARCH64_ONLY(ThreadWXEnable __wx(WXWrite, current));       \
    ThreadInVMfromJava __tiv(current);                               \
    VM_ENTRY_BASE(result_type, header, current)                      \
    debug_only(VMEntryWrapper __vew;)

class ThreadInVMfromJava : public ThreadStateTransition {
  bool _check_asyncs;
 public:
  ThreadInVMfromJava(JavaThread* thread, bool check_asyncs = true) : ThreadStateTransition(thread), _check_asyncs(check_asyncs) {
    trans_from_java(_thread_in_vm);
  }
  ~ThreadInVMfromJava()  {
    if (_thread->stack_overflow_state()->stack_yellow_reserved_zone_disabled()) {
      _thread->stack_overflow_state()->enable_stack_yellow_reserved_zone();
    }
    trans(_thread_in_vm, _thread_in_Java);
    // We prevent asynchronous exceptions from being installed on return to Java in situations
    // where we can't tolerate them. See bugs: 4324348, 4854693, 4998314, 5040492, 5050705.
    if (_thread->has_special_runtime_exit_condition()) _thread->handle_special_runtime_exit_condition(_check_asyncs);
  }
};

void trans(JavaThreadState from, JavaThreadState to)  { transition(_thread, from, to); }

  // Change threadstate in a manner, so safepoint can detect changes.
  // Time-critical: called on exit from every runtime routine
  static inline void transition(JavaThread *thread, JavaThreadState from, JavaThreadState to) {
    assert(from != _thread_in_Java, "use transition_from_java");
    assert(from != _thread_in_native, "use transition_from_native");
    assert((from & 1) == 0 && (to & 1) == 0, "odd numbers are transitions states");
    assert(thread->thread_state() == from, "coming from wrong thread state");

    // Check NoSafepointVerifier
    // This also clears unhandled oops if CheckUnhandledOops is used.
    thread->check_possible_safepoint();

    // Change to transition state and ensure it is seen by the VM thread.
    thread->set_thread_state_fence((JavaThreadState)(from + 1));

    SafepointMechanism::process_if_requested(thread);
    thread->set_thread_state(to);
  }
  
void SafepointMechanism::process_if_requested(JavaThread* thread, bool allow_suspend) {

  // Macos/aarch64 should be in the right state for safepoint (e.g.
  // deoptimization needs WXWrite).  Crashes caused by the wrong state rarely
  // happens in practice, making such issues hard to find and reproduce.
#if defined(ASSERT) && defined(__APPLE__) && defined(AARCH64)
  if (AssertWXAtThreadSync) {
    thread->assert_wx_state(WXWrite);
  }
#endif

  if (local_poll_armed(thread)) {
    process(thread, allow_suspend);
  }
}  

void SafepointMechanism::process(JavaThread *thread, bool allow_suspend) {
  // Read global poll and has_handshake after local poll
  OrderAccess::loadload();

  // local poll already checked, if used.
  bool need_rechecking;
  do {
    if (global_poll()) {
      // Any load in ::block() must not pass the global poll load.
      // Otherwise we might load an old safepoint counter (for example).
      OrderAccess::loadload();
      SafepointSynchronize::block(thread);
    }

    // The call to on_safepoint fixes the thread's oops and the first few frames.
    //
    // The call has been carefully placed here to cater to a few situations:
    // 1) After we exit from block after a global poll
    // 2) After a thread races with the disarming of the global poll and transitions from native/blocked
    // 3) Before the handshake code is run
    StackWatermarkSet::on_safepoint(thread);

    need_rechecking = thread->handshake_state()->has_operation() && thread->handshake_state()->process_by_self(allow_suspend);
  } while (need_rechecking);

  update_poll_values(thread);
  OrderAccess::cross_modify_fence();
}


// -------------------------------------------------------------------------------------------------------
// Implementation of Safepoint blocking point

void SafepointSynchronize::block(JavaThread *thread) {
  assert(thread != NULL, "thread must be set");

  // Threads shouldn't block if they are in the middle of printing, but...
  ttyLocker::break_tty_lock_for_safepoint(os::current_thread_id());

  // Only bail from the block() call if the thread is gone from the
  // thread list; starting to exit should still block.
  if (thread->is_terminated()) {
     // block current thread if we come here from native code when VM is gone
     thread->block_if_vm_exited();

     // otherwise do nothing
     return;
  }

  JavaThreadState state = thread->thread_state();
  thread->frame_anchor()->make_walkable(thread);

  uint64_t safepoint_id = SafepointSynchronize::safepoint_counter();
  // Check that we have a valid thread_state at this point
  switch(state) {
    case _thread_in_vm_trans:
    case _thread_in_Java:        // From compiled code
    case _thread_in_native_trans:
    case _thread_blocked_trans:
    case _thread_new_trans:

      // We have no idea where the VMThread is, it might even be at next safepoint.
      // So we can miss this poll, but stop at next.

      // Load dependent store, it must not pass loading of safepoint_id.
      thread->safepoint_state()->set_safepoint_id(safepoint_id); // Release store

      // This part we can skip if we notice we miss or are in a future safepoint.
      OrderAccess::storestore();
      // Load in wait barrier should not float up
      thread->set_thread_state_fence(_thread_blocked);

      _wait_barrier->wait(static_cast<int>(safepoint_id));
      assert(_state != _synchronized, "Can't be");

      // If barrier is disarmed stop store from floating above loads in barrier.
      OrderAccess::loadstore();
      thread->set_thread_state(state);

      // Then we reset the safepoint id to inactive.
      thread->safepoint_state()->reset_safepoint_id(); // Release store

      OrderAccess::fence();

      break;

    default:
     fatal("Illegal threadstate encountered: %d", state);
  }
  guarantee(thread->safepoint_state()->get_safepoint_id() == InactiveSafepointCounter,
            "The safepoint id should be set only in block path");

  // cross_modify_fence is done by SafepointMechanism::process_if_requested
  // which is the only caller here.
}

address TemplateInterpreterGenerator::generate_safept_entry_for(
        TosState state,
        address runtime_entry) {
  address entry = __ pc();
  __ push(state);
  __ call_VM(noreg, runtime_entry);
  __ dispatch_via(vtos, Interpreter::_normal_table.table_for(vtos));
  return entry;
}

void InterpreterMacroAssembler::dispatch_via(TosState state, address* table) {
  // load current bytecode
  load_unsigned_byte(rbx, Address(_bcp_register, 0));
  dispatch_base(state, table);
}

void InterpreterMacroAssembler::dispatch_base(TosState state,
                                              address* table,
                                              bool verifyoop,
                                              bool generate_poll) {
  verify_FPU(1, state);
  if (VerifyActivationFrameSize) {
    Label L;
    mov(rcx, rbp);
    subptr(rcx, rsp);
    int32_t min_frame_size =
      (frame::link_offset - frame::interpreter_frame_initial_sp_offset) *
      wordSize;
    cmpptr(rcx, (int32_t)min_frame_size);
    jcc(Assembler::greaterEqual, L);
    stop("broken stack frame");
    bind(L);
  }
  if (verifyoop) {
    interp_verify_oop(rax, state);
  }

  address* const safepoint_table = Interpreter::safept_table(state);
#ifdef _LP64
  Label no_safepoint, dispatch;
  if (table != safepoint_table && generate_poll) {
    NOT_PRODUCT(block_comment("Thread-local Safepoint poll"));
    testb(Address(r15_thread, JavaThread::polling_word_offset()), SafepointMechanism::poll_bit());

    jccb(Assembler::zero, no_safepoint);
    lea(rscratch1, ExternalAddress((address)safepoint_table));
    jmpb(dispatch);
  }

  bind(no_safepoint);
  lea(rscratch1, ExternalAddress((address)table));
  bind(dispatch);
  jmp(Address(rscratch1, rbx, Address::times_8));

#else
  Address index(noreg, rbx, Address::times_ptr);
  if (table != safepoint_table && generate_poll) {
    NOT_PRODUCT(block_comment("Thread-local Safepoint poll"));
    Label no_safepoint;
    const Register thread = rcx;
    get_thread(thread);
    testb(Address(thread, JavaThread::polling_word_offset()), SafepointMechanism::poll_bit());

    jccb(Assembler::zero, no_safepoint);
    ArrayAddress dispatch_addr(ExternalAddress((address)safepoint_table), index);
    jump(dispatch_addr);
    bind(no_safepoint);
  }

  {
    ArrayAddress dispatch_addr(ExternalAddress((address)table), index);
    jump(dispatch_addr);
  }
#endif // _LP64
}  
相关推荐
txg6661 小时前
网络安全领域简报(2026年5月9日—5月16日)
安全·web安全
山岚的运维笔记1 小时前
Bash 专业人员笔记 -- 第 11 章:`true`、`false` 和 `:` 命令
linux·运维·服务器·开发语言·笔记·学习·bash
代钦塔拉1 小时前
第一篇:字符编码全解:从ASCII/GBK/Unicode到UTF-8
开发语言·qt
Generalzy1 小时前
为什么 Go 的注释,能控制编译器?
java·python·golang
syagain_zsx1 小时前
Qt初识,快速上手
开发语言·qt
Wy_编程1 小时前
go语言面向对象和异常处理
开发语言·后端·golang
汉堡包0011 小时前
【网安干货】--调用外部链接如何防御风险
安全·网络安全
进击的荆棘1 小时前
C++起始之路——C++11(下)
开发语言·c++·c++11·lambda
Galsk1 小时前
Linux零拷贝
java·linux·服务器·面试