2.5.2 编译线程进入安全点
编译线程指的是正在执行编译优化代码的线程。JIT将一段字节码片段编译成机器码,可以想象正在执行的机器码不包含让线程主动暂停的指令,所以如果没有额外的处理,编译后的机器代码无法暂停。为了让编译后的代码能够主动暂停,一种有效的方法是在编译后的机器代码中插入一些额外的指令,这些指令可能让编译代码执行时能够主动地暂停。
对于这种方法,有两个问题需要考虑:
1)在什么地方插入额外的指令?如果插入过多的指令,可能会影响编译代码的执行速度,但是插入的指令太少,可能导致编译线程迟迟无法进入暂停状态。
2)插入的额外指令应该是什么样子的?插入指令不应该对编译优化后的机器码产生负面影响(即不影响程序正确运行),同时效率应该足够高。
对于第一个问题,在执行效率和暂停效率之间取得平衡,通常只在一些特殊位置之后才会插入特殊指令,这些特殊位置通常包含函数调用点、函数返回、循环回收等。GC安全点支持和1.4.4节OSR编译替换技术有一些相似之处,虚拟机仅在特定地方做相关功能的支持。表2-2总结了OSR和安全点支持可能发生的位置。
表2-2 OSR和GC安全点支持比较
在JVM中会在上述GC安全点支持的位置上插入额外的指令来判断是否需要暂停。一种实现是设置一个全局状态标记,当需要线程暂停时修改状态值,额外指令可以判断状态是否发生变化,如果发生变化,则进入安全状态并暂停线程的执行。
JVM在Linux中的实现很有代表性,首先在JVM初始化时产生一个全局的轮询页面(Polling Page),当需要编译线程进入安全点时,该轮询页面会被设置为不可读。编译线程在执行过程中如果执行到检查轮询页面的状态,并发现页面不可读,则会产生一个信号量(SIGSEGV),JVM捕获信号量保存编译线程的状态,然后暂停自身的执行,待GC执行结束后恢复状态继续执行。
需要注意的是,编译代码可能访问堆中的对象,而进入安全点以后,GC执行可能会修改对象的位置及引用关系,所以在GC执行中需要对编译代码中引用的对象更新对象引用关系。为了更准确地支持编译后代码对象引用关系的更新,通常需要额外的数据结构存储对象的位置。
在编译代码中需要针对循环进行额外处理,否则遇到一个超大循环时可能导致编译线程长时间无法进入安全点,但是也不需要在循环的回边中每次都插入额外的指令,那样做会影响效率。一种可行的方法是每经过一定循环次数后执行额外的检查指令,在JVM中使用参数UseCountedLoopSafepoints控制是否允许循环间隔检查,并且提供了参数(LoopStripMiningIter)控制循环间隔的步长(默认值为1000),如果发现编译线程长时间无法进入安全点,则可以尝试使用这两个参数进行调整。