TL; DR HotSpot JIT优化消除了计数循环中的安全点检查,这是一个恶作剧。
这是一个非常有趣的问题:一个简单的Java测试揭示了JVM内部的一个重要问题。
没有立即出现线程转储的事实表明该问题不在Java代码中,而是与JVM相关。线程转储在安全点打印。延迟意味着VM无法在短时间内到达安全点。
背景
当没有Java线程在运行时,某些VM操作(GC,Deoptimization,Thread
dump和某些其他操作)在世界停顿时执行。但是Java线程无法在任意点停止,它们只能在称为 安全
点的某些位置暂停。在JIT编译的代码中,安全点通常放置在方法出口和后向分支(即循环内部)中。
就性能而言,安全点检查相对便宜,但并非免费。这就是为什么JIT编译器试图在可能的情况下减少安全点的数量。一种这样的优化是消除计数循环中的安全点检查,即已知具有有限次数的迭代的具有整数计数器的循环。
验证理论
让我们回到测试中,检查是否及时到达安全点。
添加
-XX:+SafepointTimeout-XX:SafepointTimeoutDelay=1000JVM选项。每当VM在1000毫秒内未达到安全点时,这应该会打印一条调试消息。
# SafepointSynchronize::begin: Timeout detected:# SafepointSynchronize::begin: Timed out while spinning to reach a safepoint.# SafepointSynchronize::begin: Threads which did not reach the safepoint:# "pool-1-thread-2" #12 prio=5 os_prio=0 tid=0x0000000019004800 nid=0x1480 runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE# SafepointSynchronize::begin: (End of list)
是的,它打印出线程
pool-1-thread-2无法在1000毫秒内停止。这是执行程序池的第二个线程,应运行
insertionSort算法。
insertionSort有两个非常长的嵌套计数循环,看起来JVM消除了其中的安全点检查。因此,如果此方法以编译模式运行,则JVM在该方法完成之前无法停止它。如果在方法运行时请求世界暂停,则所有其他线程也将等待。
该怎么办?
这个问题早已为人所知。这是相关的JVM错误:JDK-5014723。这不是一个高度优先的问题,因为该问题很少出现在现实应用中。
JDK 8u92中出现了一个新的JVM标志,以解决此问题。
-XX:+UseCountedLoopSafepoints总是将安全点检查放在循环内。
另一种解决方案是通过修改循环中的计数器变量将长计数循环转换为通用循环。
例如,如果您替换为问题
if (breaker) break;,
if (breaker) j = 0;该问题也将消失。
那为什么它在调试模式下工作呢?
在启动调试器的情况下启动JVM时,将禁用某些JIT优化,以使调试信息可用。在这种情况下,编译后的代码具有所有安全点检查。



