HotSpot虚拟机采用解释器与编译器并存的架构,HotSpot内置了两种即时编译器,分别称为Client Compiler和Server Compiler或则简称为C1编译器和C2编译器。采用编译器和解释器搭配使用的方式在虚拟机中称为“混合模式(Mixed Mode)”。
解释器能够迅速的执行程序(编译代码耗时),而编译器编译成的本地代码运行效率更高,但两种编译器也有差异:Client Compiler的编译速度更快,但是Server Compiler编译质量更好。为了在程序启动响应速度与运行效率之间达到最佳平衡,HotSpot虚拟机使用了分层编译策略,在server模式下-XX:+TieredCompilation
将开启该功能,默认已开启。分层编译会根据编译器编译、优化的规模与耗时,配合解释器、C1、C2来划分出不同的编译层次来优化效率。
由于即时编译器(Just In Time Compiler, JIT)没有时间编译程序中的所有方法,因此所有代码最初都是在解释器中运行。一旦方法被调用的次数变多,就可能变成编译。这个过程是由HotSpot VM中与每个方法关联的计数器来控制的。每个方法都有两个计数器:方法调用计数器和回边计数器。方法调用计数器在每次进入方法时加一;回边计数器在控制流每次从行号靠后的字节码跳到靠前的字节码时加一。与仅用方法调用计数器相比,用回边计数器可以检测包含循环的方法,能使这些方法更早地转为编译。
当一个方法被调用时,会先检查该方法是否存在被JIT编译过的版本,如果存在,则优先使用编译后的本地代码来执行。如果不存在已编译过的版本,则将此方法的调用计数器加1,然后判断方法调用计数器和回边计数器值之和是否超过方法调用计数器的阈值,该阈值可以通过参数-XX:CompileThreshold
来指定。如果已超过阈值,那么将会向即时编译器发起一个该方法的代码编译请求。
当发起编译请求时,要编译的方法会进入被一个或多个编译器线程监视的队列。如果编译器线程不忙,就会从队列中移出一个编译请求开始编译。通常解释器不会等编译结束,相反,它会重置方法调用计数器,然后在解释器中执行该方法。一旦编译完成,编译代码就会和该方法关联,然后下次调用时就会使用该编译代码。通常来说,不等编译完成仍继续执行是个好办法,因为执行和编译可以继续并行。如果你想让解释器等编译完成,可以使用HotSpot VM命令行选项-Xbatch
或-XX:-BackgroundCompilation
阻塞执行,等待编译完成。
如果不做任何设置,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间之内方法被调用的次数。当超过一定的时间时限,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那么该方法的调用计数器就会被减少一半,这个过程称为方法调用计数器热度的衰减,而这段时间就称为此方法统计的半衰周期。可以使用虚拟机参数-XX:UseCounterDecay
来关闭热度衰减,让方法计数器统计方法调用的绝对次数,可以使用-XX:CounterHalfLifeTime
参数设置半衰周期的时间,单位是秒。
当解释器遇到一条回边指令时,会先查找将要执行的代码片段是否有已经编译好的版本,如果有,它将会优先执行已经编译的代码,否则就把回边计数器的值加1,然后判断方法调用计数器与回边计数器值之和是否超过回边计数器的阈值。在server模式下回边计数器的阈值是CompileThreshold(方法调用计数器阈值) * (OnStackReplacePercentage(OSR比率)– InterpreterProfilePercentage(解释器监控比率)) /100
。当超过阈值的时候,就会提交一个栈上替换(On Stack Replacement,OSR)编译请求。并且把回边计数器的值降低,以便继续在解释器中执行循环。与方法调用计数器不同,回边计数器没有计数热度衰减的过程。
当回边计数器溢出时,编译器会发起编译请求,这次编译从回边的字节码开始而不是从方法的首个字节码开始。然后以解释器帧作为输入生成代码,并从此状态开始执行。在这种情况下,长时间运行的循环可以充分利用编译代码。这种以解释器帧作为输入执行的代码生成技术称为栈上替换。