news 2026/4/21 3:23:57

专家视角看JVM是如何让所有高速运行的线程“瞬间静止”

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
专家视角看JVM是如何让所有高速运行的线程“瞬间静止”

专家视角看JVM是如何让所有高速运行的线程“瞬间静止”

    • 前言
    • 当 GC 发生时,JVM 是如何让所有正在高速运行的线程“瞬间静止”的
      • 1. 核心指挥官:`SafepointSynchronize::begin()`
      • 2. 线程如何感应:主动轮询机制(Polling)
        • A. 解释执行模式(Interpreted)
        • B. JIT 编译模式(Compiled)
        • C. 源码参考:`ThreadStateTransition`
      • 3. 信号处理:从段错误到“静止”
      • 4. 线程状态的“分而治之”
      • 5. 源码关键点回顾 (hotspot/src/share/vm/runtime/safepoint.cpp)
      • 6. 思考:为什么会有“Safepoint 导致的长停顿”?
      • 总结:静止的真谛
      • 题外话
        • 为什么不直接用 `pthread_suspend`?

前言

本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限,文中内容难免存在疏漏,恳请读者不吝指正。


当 GC 发生时,JVM 是如何让所有正在高速运行的线程“瞬间静止”的

在 JVM 的世界里,让所有线程“瞬间静止”的操作,是一场由VM Thread发起、全体 Java 线程高度配合的“优雅停步”。Global Safepoint(全局安全点)机制是实现“Stop The World”(STW)的幕后推手。我们需要看穿 OpenJDK 8如何利用操作系统的内存分页(Memory Paging)和硬件指令级的巧妙配合,将数千个高速运转的线程在毫秒级时间内精准拦截。

在 OpenJDK 8源码中,这一机制的实现核心隐藏在safepoint.cppinterpreterRuntime.cpp以及 JIT 编译器的指令序列中。


1. 核心指挥官:SafepointSynchronize::begin()

当 GC 线程(如 CMS、G1 的标记阶段或 Parallel GC 的整理阶段)发出 STW 请求时,VM Thread(一个特殊的内核线程)会调用hotspot/src/share/vm/runtime/safepoint.cpp中的SafepointSynchronize::begin()方法。

这是“静止”过程的总控逻辑:

  1. 设置状态位:将全局变量_state设为_synchronizing
  2. 翻转轮询页(Polling Page)权限:这是最绝妙的物理操作。JVM 会通过os::make_polling_page_unreadable()调用系统的mprotect,将一个预先分配好的4KB 轮询页面设置为“不可读”。
  3. 等待所有线程挂起:VM Thread 会循环检查_waiting_to_block计数器,直到所有正在运行的线程都进入安全点状态。

2. 线程如何感应:主动轮询机制(Polling)

Java 线程并不是被动被中断的,它们在高速运行时会不断地“查看”自己是否需要停下来。为了保证性能,这种查看必须极快。为了保证性能,JVM 不能在业务代码里加锁。它采用了一种**“主动轮询”**的硬件策略。

A. 解释执行模式(Interpreted)

在解释器中,JVM 在字节码的分派表(Dispatch Table)中做了手脚。在hotspot/src/cpu/x86/vm/templateTable_x86_64.cpp中,解释器在执行特定的字节码(如returngotobackward branch)时处插入一段逻辑。它会去读取一个特定的内存地址——Polling Page

  • SafepointSynchronize开启时,解释器会将当前的执行跳转到InterpreterRuntime::at_safepoint
  • 每当执行完一条字节码,或者在循环的跳转(Backedge)处,解释器都会检查这个状态。
B. JIT 编译模式(Compiled)

对于经过 C1/C2 优化的代码,JVM 会在代码块中插入物理指令。

  • 指令插入点:通常在方法返回处(Return)非计数循环的回跳处(Loop backedge)
  • 汇编代码:JIT 编译器(C1/C2)会在生成的机器码中插入一条看似无用的指令。
    • 正常情况下:这个地址(Polling Page)是可读的,test指令执行只需几个 CPU 周期,几乎零开销。
    • GC 发生时:如前所述,VM Thread 将该页设为不可读。此时,任何执行到这里的线程都会触发一个SIGSEGV(段错误)信号。
    • 在 x64 架构下,它通常长这样:
      test %eax, 0x1601000 ; 0x1601000 是 Polling Page 的虚拟地址
C. 源码参考:ThreadStateTransition

interfaceSupport.hpp中,ThreadStateTransition::transition负责在进入/离开 VM 时检查安全点。如果 GC 正在进行,该宏会阻塞线程,防止其在不恰当的时机回到 Java 世界。


3. 信号处理:从段错误到“静止”

当线程触发了 SIGSEGV 信号,它并不会崩溃,而是控制权瞬间转移到操作系统的信号处理器(Signal Handler)。JVM 在os_linux_x86.cpp中预先注册了JVM_handle_linux_signal

  • 路径hotspot/src/os/linux/vm/os_linux.cpp中的JVM_handle_linux_signal

  • 逻辑:信号处理器识别出这是由于 Safepoint Polling Page 导致的异常,于是它会将该线程的上下文(Context)保存起来,并将线程状态从_thread_in_Java切换为_thread_blocked

  • 阻塞:线程随后会在一个信号量或条件变量上wait,直到 GC 完成,VM Thread 重新将轮询页设为可读并发出唤醒信号。

  • 简单的处理流程

    1. 识别陷阱:信号处理器发现报错地址正是 Polling Page。
    2. 上下文重定向:它不是让程序崩溃,而是修改该线程的指令指针(RIP),将其导向一段预先备好的代码:SafepointSynchronize::block()
    3. 线程状态转换:在block()内部,线程会将自己的状态从_thread_in_Java切换为_thread_blocked

4. 线程状态的“分而治之”

JVM 并不需要等“所有”线程都执行到轮询点。根据线程当前所处的环境,JVM 采取了不同的策略:

线程当前状态JVM 处理策略是否需要等待其到达轮询点?
执行 Java 字节码必须等待执行到下一个test指令或字节码边界。(这是导致 Safepoint 停顿过长的主要原因)。
执行 Native 代码 (JNI)它们已经不在 Java 环境中,不影响 GC。但当它们从 JNI 返回欲切回 Java 状态时,会检查全局安全点标志。如果正在 GC,它们会原地“挂起”。。但该线程从 Native 返回 Java 时会检查状态并阻塞。
已阻塞 (Blocked/Waiting)视为已在安全点。VMThread只需要确认它们的状态并将其“锁定”,不允许在 GC 期间唤醒即可。
正在执行 VM 内部逻辑会在完成当前原子操作后,在ThreadStateTransition处检查状态并自愿挂起。

5. 源码关键点回顾 (hotspot/src/share/vm/runtime/safepoint.cpp)

// 简化后的逻辑逻辑voidSafepointSynchronize::begin(){// 1. 改变状态_state=_synchronizing;// 2. 使轮询页不可读 (触发 SIGSEGV)os::make_polling_page_unreadable();// 3. 循环等待所有线程while(_waiting_to_block>0){// 这里的等待通常伴随有重试和超时逻辑Threads_lock->wait(true,1);}// 4. 此时,所有线程已静止,GC 开始!_state=_at_safepoint;}

6. 思考:为什么会有“Safepoint 导致的长停顿”?

我们需要识别出这种机制的局限性:

  1. 长循环陷阱
    在 OpenJDK 8 中,如果是那种for (int i=0; i<int_max; i++)这种受限循环(Counted Loop),JIT 编译器默认不会在循环体内插入轮询点。如果循环体执行时间很长,整个 JVM 必须等这个循环跑完才能进入 Safepoint。
    • 对策:使用-XX:+UseCountedLoopSafepoints参数。
  2. 内存页权限切换开销
    虽然test指令很快,但mprotect系统调用和随后的信号处理(Signal Handling)是有成本的,尤其是在数千个线程并发时。
  3. 计算密集型任务:大量纯数值计算可能导致轮询点间隔过长。
  4. JNI 阻塞:虽然 Native 代码不阻止 GC,但如果 Native 代码运行时间极长,它持有的局部引用可能导致 GC 扫描耗时增加。

总结:静止的真谛

JVM 让线程“瞬间静止”的本质是:利用 OS 的内存保护机制,将一个软件层面的状态检查(“我该停吗?”)转化为一个硬件级别的陷阱(Trap)。

这套设计的精妙之处在于,在 99% 的运行时间内,线程几乎不需要付出任何代价就能保持高度的警觉。当 GC 降临时,它又利用了操作系统的信号机制,强制原本互不干涉的线程整齐划一地进入阻塞状态。


题外话

为什么不直接用pthread_suspend

很多资深开发者会问:为什么不直接利用操作系统的信号强制挂起线程?

  1. 一致性:强制挂起可能使线程停在任何地方(比如正在修改一个 JVM 内部数据结构的中途)。通过安全点,JVM 确保线程停在“代码边界”上,此时对象的引用关系是清晰、可枚举的。
  2. 性能:系统调用的上下文切换成本极高。基于内存页权限触发的异常处理(SIGSEGV)在正常运行时几乎是零开销的。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/21 3:22:14

园区网络实验作业

一、实验拓扑二、实验要求需求&#xff1a; 1、按照图示的VLAN及IP地址需求&#xff0c;完成相关配置 2、要求SW1为VLAN 2/3的主根及主网关SW2为vlan 20/30的主根及主网关SW1和SW2互为备份 3、可以使用super vlan&#xff08;但是会出现双主&#xff09;可忽略 4、上层通过静态…

作者头像 李华
网站建设 2026/4/21 3:21:17

深亚微米ASIC设计的技术挑战与低功耗解决方案

1. 深亚微米ASIC设计的技术挑战与应对策略在半导体工艺进入45nm及以下节点后&#xff0c;ASIC设计面临着一系列前所未有的技术挑战。这些挑战主要来自四个方面&#xff1a;漏电功耗、时序收敛、信号完整性和可测试性设计。作为从业十余年的芯片设计工程师&#xff0c;我将结合I…

作者头像 李华
网站建设 2026/4/21 3:20:24

Dify工业知识库冷启动难题破解:仅需3人·2天·1台国产服务器,完成某汽车零部件集团全厂知识纳管

第一章&#xff1a;Dify工业知识库冷启动实战概览工业场景下的知识库建设常面临原始文档分散、格式不一、语义模糊等挑战。Dify 提供低代码、可编排的 RAG 工作流能力&#xff0c;为工业知识库的冷启动提供了轻量级、高可控性的技术路径。本章聚焦从零构建一个面向设备维修手册…

作者头像 李华
网站建设 2026/4/21 3:18:33

FlowState Lab在物联网数据分析中的应用:预测设备传感器信号异常

FlowState Lab在物联网数据分析中的应用&#xff1a;预测设备传感器信号异常 1. 工业物联网的痛点与机遇 工厂车间里&#xff0c;一台价值百万的数控机床正在全速运转。突然&#xff0c;主轴轴承温度异常升高&#xff0c;短短几分钟内&#xff0c;这台关键设备彻底瘫痪。生产…

作者头像 李华