传统编译只需要为源代码生成对应的机器代码即可,而即时编译是与运行时密切相关的,即编译器需要考虑在何种情况下进行编译、编译完成后机器代码如何被虚拟机使用等。接下来将简单介绍即时编译涉及的一些技术。
非此即彼的两个即时编译器可能不是最佳选择。那么,是否有一种编译技术可以综合实现解释器的快速启动、C1的快速预热和C2的高性能产出呢?可以使用-XX:+TieredCompilation开启分层编译,它额外引入了四个编译层级。
1)第0级:解释执行。
2)第1级:C1编译,开启所有优化(不带Profiling)。
3)第2级:C1编译,带调用计数和回边计数的Profiling信息(受限Profiling)。
4)第3级:C1编译,带所有Profiling信息(完全Profiling)。
5)第4级:C2编译。
常见的分层编译层级转换路径如图7-1所示。
在JDK 7及之前版本中,可以使用-XX:CompileThreshold=<val>调整编译一个方法的时机,但是在开启分层编译后,该参数会被忽略,判断一个方法是否编译的条件不再基于单个参数,而是综合考虑一系列因素和多个参数,类似于下面的公式:
Predicate(i, b) = (i > Tier3InvocationThreshold)或者(i > Tier3MinInvocationThreshold and i+b> Tier3CompileThreshold)
其中i表示方法调用的次数,b表示回边发生的次数,-XX:Tier3ComilpeThreshold默认为2000。
模板解释器使用方法计数和回边计数识别热点,其中,方法计数识别热点方法,回边计数识别热点循环,如图7-2所示。
一个合理的猜想是编译器识别出热点代码然后进行编译,等待编译完成,在下一次调用时,可直接调用编译后的机器代码代替解释执行。
但在实际情况中并不总是有“下一次调用”的机会。假如有一个包含了千万次的循环方法,方法只执行一次,此时如果等待方法执行完成再进行编译,由于方法只调用一次,编译器将没有机会使用编译后的代码。
为了防止编译器做这种无用功,需要一种技术在解释执行循环期间将代码替换为编译后的代码,即循环的第N次使用解释执行,第N+1次使用编译后的代码,这样就能将“下一次调用”缩小到“下一次循环”。这种技术叫作栈上替换(On Stack Replacement,OSR)。OSR机制类似协程切换,它将解释器栈的数据打包到OSR buffer,然后在编译后的代码里面提取OSR buffer的数据放入编译后的执行栈,再继续执行。
虚拟机执行方法或循环的次数越多,它知道的代码的额外信息就越多。假设虚拟机执行了很多次obj.equals()发现obj的类型都是String,那么虚拟机可以乐观地认为obj就是String类型,继而直接调用String.equals,省去了查询obj虚函数表的开销。但是如果后续变量obj接收到其他类型的对象,虚拟机也必须有处理这种少数情况的能力,这种处理少数情况的行为即退优化。
除了上述这个例子外,编译器优化还会做很多乐观的假设,它广泛使用fast/slow惯例,乐观地认为大部分情况程序都是走快速路径fast,而只有极少数情况走慢速路径slow。当极少数情况发生时,虚拟机将执行退优化,使用慢速路径作为后备方案。退优化可以认为是栈上替换的逆操作。
本文就是愿天堂没有BUG给大家分享的内容,大家有收获的话可以分享下,想学习更多的话可以到微信公众号里找我,我等你哦。