当前位置: 首页 > news >正文

JVM常用概念之安全点轮询

问题

在需要进行标记、压缩、清理时,JVM 是如何停止 Java 线程以进行STW的?

基础知识

假设您拥有像 JVM 这样的托管运行时,并且您需要偶尔停止 Java 线程以运行一些运行时代码。例如,您想要执行STW的 GC。您可以等待所有线程最终调用 JVM,例如,请求分配(通常是TLAB重新填充),或输入一些本地方法(转换为本地会捕获它),或执行其他操作。但这并不能保证会发生!如果线程当前正在某种忙循环中运行,从未执行任何特殊操作怎么办?

在大多数机器上,停止正在运行的线程其实很简单:您可以向它发送信号、强制处理器中断等,使其停止线程正在执行的操作并将控制权转移到其他地方。但是,Java 线程在任意点停止通常是不够的,特别是当您想要精确的垃圾收集时。在那里,您想知道寄存器和堆栈中有什么,以防这些值实际上是您需要处理的对象引用。或者,如果您想取消锁定,您需要获得有关线程状态和获取的锁定的精确信息。或者,如果您对方法进行反优化,您确实希望从安全位置执行此操作,而不会丢失已执行的代码部分和/或临时值。

因此,现代 JVM(如 Hotspot)实现了协作方案:线程在其生命周期的某些已知点(当其状态已知时)不时询问是否应将控制权移交给 VM。当所有线程都停止在这些已知点时,VM 即达到安全点。因此,检查安全点请求的代码片段称为安全点轮询。

实现需要满足有趣的权衡:安全点轮询几乎从不触发,因此当未触发时它们应该非常高效。

实验

源码

import org.openjdk.jmh.annotations.*;

import java.util.concurrent.TimeUnit;

@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(3)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
public class EmptyBench {
    @Benchmark
    public void emptyMethod() {
        // This method is intentionally left blank.
    }
}

您可能认为此基准测试测量的是空方法,但实际上它测量的是为基准测试服务的最小基础结构代码:计算迭代次数并等待迭代时间结束。幸运的是,这段代码相当快,因此可以借助-prof perfasm对其进行完整分析。执行结果如下:

3.60%  ↗  ...a2: movzbl 0x94(%r8),%r10d       ; load "isDone" field
0.63%  │  ...aa: add    $0x1,%rbp             ; iterations++;
32.82% │  ...ae: test   %eax,0x1765654c(%rip) ; global safepoint poll
58.14% │  ...b4: test   %r10d,%r10d           ; if !isDone, do the cycle again
       ╰  ...b7: je     ...a2

上述结果说明,空方法被内联,其中的所有内容都消失了,只有基础设施保留了下来。

看到那个“全局安全点轮询”了吗?当需要安全点时,JVM 会启用“轮询页面”,因此任何读取该页面的尝试都会触发分段错误 (SEGV) 。当 SEGV 最终从这个安全点轮询触发时,控制权将首先传递给任何现有的 SEGV 处理程序,而 JVM 已经准备好了一个!例如,查看JVM_handle_linux_signal是如何做到这一点的。

所有这些技巧的目的是使安全点轮询尽可能便宜,因为它们需要在很多地方发生,而且它们几乎总是不会触发。因此,使用test %eax, (addr) :当未触发安全点轮询时,它没有效果。它还具有非常紧凑的编码,在 x86_64 上“仅” 6 个字节。轮询页面地址对于给定的 JVM 进程是固定的,因此该进程中 JIT 生成的代码可以使用RIP 相对寻址:它表示页面位于当前指令指针的给定偏移量处,从而无需花费宝贵的字节来编码绝对的 8 字节地址。

通常,还会有一个轮询页面一次处理所有线程,因此生成的代码不必区分当前正在运行的线程。但是,如果 VM 想要停止单个线程怎么办? JEP-312:“线程本地握手”回答了这个问题。它为 VM 提供了触发单个线程握手轮询的功能,目前实现方法是为每个线程分配单独的轮询页面,然后轮询指令从线程本地存储中读取该页面地址。

这纯粹是运行时的考虑,因此可以使用-XX:-ThreadLocalHandshakes禁用它,然后生成的代码将与 8u191 中的代码相同。这解释了为什么这个基准测试在 8 和 11 上的表现不同(让我们立即在-prof perfnorm下运行它):

Benchmark                              Mode  Cnt  Score   Error  Units

# 8u191
EmptyBench.test                        avgt   15   0.383 ±  0.007  ns/op
EmptyBench.test:CPI                    avgt    3   0.203 ±  0.014   #/op
EmptyBench.test:L1-dcache-load-misses  avgt    310⁻⁴            #/op
EmptyBench.test:L1-dcache-loads        avgt    3   2.009 ±  0.291   #/op
EmptyBench.test:cycles                 avgt    3   1.021 ±  0.193   #/op
EmptyBench.test:instructions           avgt    3   5.024 ±  0.229   #/op

# 11.0.1
EmptyBench.test                        avgt   15   0.590 ±  0.023  ns/op ; +0.2 ns
EmptyBench.test:CPI                    avgt    3   0.260 ±  0.173   #/op
EmptyBench.test:L1-dcache-loads        avgt    3   3.015 ±  0.120   #/op ; +1 load
EmptyBench.test:L1-dcache-load-misses  avgt    310⁻⁴            #/op
EmptyBench.test:cycles                 avgt    3   1.570 ±  0.248   #/op ; +0.5 cycles
EmptyBench.test:instructions           avgt    3   6.032 ±  0.197   #/op ; +1 instruction

# 11.0.1, -XX:-ThreadLocalHandshakes
EmptyBench.test                        avgt   15   0.385 ±  0.007  ns/op
EmptyBench.test:CPI                    avgt    3   0.205 ±  0.027   #/op
EmptyBench.test:L1-dcache-loads        avgt    3   2.012 ±  0.122   #/op
EmptyBench.test:L1-dcache-load-misses  avgt    310⁻⁴            #/op
EmptyBench.test:cycles                 avgt    3   1.030 ±  0.079   #/op
EmptyBench.test:instructions           avgt    3   5.031 ±  0.299   #/op

因此,线程本地握手会增加另一个 L1 命中负载,这大约需要半个周期。这也为我们估算安全点轮询本身的成本提供了一些依据:它本身就是 L1 命中负载,可能需要另外半个周期。

总结

安全点和握手轮询是托管运行时实现中一些有趣的细节。它们经常出现在生成的代码中的热路径上,有时会影响性能,尤其是在紧密循环中。然而,它们的存在对于运行时实现精确垃圾回收、锁定优化、去优化等重要功能是必要的。

相关文章:

  • TensorRT:高性能深度学习推理的利器
  • DeepSeek私有化部署与安装浏览器插件内网穿透远程访问实战
  • 轨道交通3U机箱CPCI电机控制板(DSP),主要运行控制算法以对牵引电机进行精准的运动控制
  • 【Go语言圣经5.1】
  • 微前端 无界wujie
  • 蓝牙技术联盟中国实体成立!华为、小米发声支持本土化战略
  • QAI AppBuilder 快速上手(7):目标检测应用实例
  • CVPR2025自动驾驶端到端前沿论文汇总
  • Docker命令解析:加速你的容器化之旅(以Nginx为例)
  • 设计模式Python版 访问者模式
  • 串口烧录出现频繁回复乱码 频繁回复一个数字且烧录失败 字节混乱
  • 【docker】部署MySQL容器
  • [从零开始学SSM] Bean的配置
  • 迭代法反转链表
  • 光猫 和 全光 WiFi
  • 13个问题
  • 【大模型学习】第二十三章 深度解析BERT
  • 【工具/调研】各种类型文件转PDF
  • 9、讲一讲你理解的虚拟内存【中高频】
  • 【Linux】设置系统时间
  • “走进书适圈”:一周城市生活
  • 师爷、文士、畸人:会稽范啸风及其著述
  • 沪指跌0.68%报3380.82点,创指跌1.92%:券商、军工跌幅靠前
  • 机构发布“2025中国高职院校排名”
  • 被前男友泼汽油致残后,一个女孩经历的双重灼烧
  • 微软将裁员3%,减少管理层