JEP 312:线程本地握手
概括
引入一种在线程上执行回调而不执行全局 VM 安全点的方法。使停止单个线程而不仅仅是所有线程或没有线程变得可能且便宜。
非目标
在所有支持的架构上有效地实现这一点可能不可行。最初的目标并不是支持所有处理器架构和所有版本的处理器架构。
成功指标
-
在标准基准测试中,新机制不会产生超过 1% 的性能开销。
-
新机制不会增加到达传统全球安全点所需的时间。
动机
能够停止单个线程有多种应用:
-
改进偏向锁撤销,仅停止单个线程来撤销偏向,而不是停止所有线程。
-
减少不同类型的可服务性查询(例如获取所有线程的堆栈跟踪)对 VM 总体延迟的影响,这在具有大量 Java 线程的 VM 上可能是一项缓慢的操作。
-
通过减少对信号的依赖来执行更安全的堆栈跟踪采样。
-
通过与 Java 线程执行握手,使用所谓的非对称 Dekker 同步技术消除一些内存障碍。例如,G1 固有地需要并由 CMS 使用的条件卡标记代码将不需要内存屏障。因此,可以优化 G1 写后屏障,并且可以删除试图避免内存屏障的分支。
所有这些都将帮助虚拟机通过减少全局安全点的数量来实现更低的延迟。
描述
握手操作是在每个 JavaThread 处于安全点安全状态时为该线程执行的回调。回调由线程本身或 VM 线程执行,同时保持线程处于阻塞状态。安全指向和握手之间的最大区别在于,每个线程的操作将尽快在所有线程上执行,并且一旦其自己的操作完成,它们将继续执行。如果已知 JavaThread 正在运行,则也可以与该单个 JavaThread 执行握手。
在最初的实施中,在给定时间飞行中最多只能进行一次握手操作。然而,该操作可以涉及所有 JavaThread 的任何子集。 VM线程将通过VM操作来协调握手操作,这将有效地防止在握手操作期间发生全局安全点。
当前的安全点方案被修改为通过每线程指针执行间接寻址,这将允许单个线程的执行被强制捕获在保护页上。本质上,任何时候都会有两个轮询页面:一个始终受到保护,另一个始终不受保护。为了强制线程让出,VM 更新相应线程的每线程指针以指向受保护的页面。
线程本地握手最初将在 x64 和 SPARC 上实现。其他平台将回落到正常的安全点。新的产品选项-XX:ThreadLocalHandshakes
(默认值true
)允许用户在支持的平台上选择正常的安全点。
备择方案
考虑了多种替代方案:
-
相反,发出条件分支。这会消耗分支预测器状态,并且不像负载那样紧张。该领域的实验表明,条件分支的性能高度依赖于目标 CPU 的特定微体系结构。条件分支方法的另一个缺点是每个条件分支安全点都需要输出相应的存根以负责返回轮询的位置。
-
有一个想法意味着牺牲另一个寄存器,然后将寄存器保存的地址加载到寄存器本身,假设寄存器的内容是其自己的线程本地字段的地址。通过将该字段更改为 来启动线程本地握手
NULL
。下一次轮询寄存器将被设置为NULL
,并且对于第二次轮询,负载将被捕获。这需要全局牺牲一个寄存器,陷阱的成本更高,并且一旦发出停止线程的请求,平均需要两倍的轮询才能到达安全点。好处是理论上它对应用程序执行的影响较小。 -
之前构建的原型中,全局轮询页面保持原样,但虚拟机代码中仅捕获实际的目标线程。不是握手目标的线程将简单地从信号处理程序返回并继续执行。这种方法的一个缺点是,如果目标线程响应缓慢,则可能会导致其他 Java 线程的信号风暴,因为在目标线程响应之前无法解除轮询页面。