0%

即时编译

profiling

分层编译中的1层、3层、4层都会进行profiling,收集能够反映程序执行状态的数据。其中最为基础的便是方法的调用次数以及循环回边的执行次数,它们被用于触发即时编译。

1层和4层还会收集用于5层C2编译的数据,如分支跳转字节码的分支profile(branch profile),包括跳转次数和不跳转次数,以及非私有实例方法调用指令、强制类型转换checkcast指令、类型测试instanceof指令、引用类型的数组存储aastore指令的类型profile(receiver type profile)。

分支profile和类型profile的收集将给应用程序带来不少的性能开销。正是因为这部分额外的profiling,是的4层C1代码的性能比3层C1代码的性能低30%。

通常情况下,不会在解释执行过程中收集分支profile以及类型profile。只有在方法触发C1编译后,JVM认为该方法有可能被C2编译,方才在该方法的C1代码中收集这些profile。

只有在比较极端的情况下,如等待C1编译的方法数目太多时,JVM才会开始在解释执行过程中收集这些profile。

C2可以根据收集得到的数据进行猜测,假设接下来的执行同样会按照所收集的profile进行,从而作出比较激进的优化。

基于分支profile的优化

根据条件跳转指令的分支profile,即时编译器可以将从未执行过的分支剪掉,以避免编译这些很有可能不会用到的代码,从而节省编译时间以及部署代码所要消耗的内存空间。此外,“剪枝”将精简程序的数据流,从而触发更多的优化。

在现实中,分支profile出现仅跳转或者仅不跳转的情况并不多见。即时编译器对分支profile的利用也不仅限于“剪枝”。它还会根据分支profile,计算每一条程序执行路径的概率,以便某些编译器优化优先处理概率较高的路径。

基于类型profile的优化

关于instanceof以及方法调用的类型profile。

如果instanceof的目标类型是final类型,那么JVM仅需要比较测试对象的动态类型是否为该final类型。对象头存有该对象的动态类型。获取对象的动态类型仅为单一的内存读指令。

如果目标类型不是final类型,那么JVM需要从测试对象的动态类型开始,一次测试该类、该类的父类、祖先类,该类所直接实现或者间接实现的接口是否与目标类型一致。

和基于分支profile的优化一样,基于类型profile的优化同样也是作出假设,从而精简控制流以及数据流。这两者的核心都是假设。

对于分支profile,即时编译器假设的是仅执行某一分支;对于类型profile,即时编译器假设的是对象的动态类型仅为类型profile中的那几个。

去优化

假设失败就去优化

当假设失败的情况下,JVM给出的解决方案是去优化,即从执行即时编译生成的机器码切换回解释执行。

在生成的机器码中,即时编译器将在假设失败的位置上插入一个陷阱(trap)。该陷阱实际上是一条call指令,调用至JVM里专门负责去优化的方法。与普通的call指令不一样的是,去优化方法将更改栈上的返回地址,并不再返回即时编译器生成的机器码中。

去优化的过程相当复杂。由于即时编译器采用了许多优化方式,其生成的代码和原本的字节码的差异非常之大。在去优化的过程中,需要将当前机器码的执行状态转换至某一字节码之前的执行状态,并从该字节码开始执行。这便要求即时编译器在编译过程中记录好这两种执行状态的映射。

例:经过逃逸分析之后,机器码可能并没有实际分配对象,而是在各个寄存器中存储该对象的各个字段(标量替换)。在去优化过程中,JVM需要还原出这个对象,以便解释执行时能够使用该对象。

对于标量替换的新建对象,机器码会在去优化时重建对象。

当根据映射关系创建好对应的解释执行栈帧后,JVM便会采用OSR技术,动态替换栈上的内容,并在目标字节码处开始解释执行。

去优化是否保留机器码

在调用JVM的去优化方法时,即时编译器生成的机器码可以根据产生去优化的原因来决定是否保留这一份机器码,以及何时重新编译对应的Java方法。

如果去优化的原因与优化无关,即使重新编译也不会改变生成的机器码,那么生成的机器码可以在调用去优化方法时传入Action_None,表示保留这一份机器码,在下一次调用该方法时重新进入这一份机器码。

如果去优化的原因与静态分析的结果有关,例如类层次分析,那么生成的机器码可以在调用去优化方法时传入Action_Recompile,表示不保留这一份机器码,但是可以不经过重新profile,直接重新编译。

如果去优化的原因与基于profile的激进优化有关,那么生成的机器码需要在调用去优化方法时传入Action_Reinterpret,表示不保留这一份机器码,而且需要重新收集程序的profile。

这是因为基于profile的优化失败的时候,往往代表着程序的执行状态发生改变,因此需要更正已收集的profile,以便更好的反映新的程序执行状态。

总结

通常情况下,解释执行过程中仅收集方法的调用次数以及循环回边的执行次数。

当方法被4层C1所编译时,生成的C1代码将收集条件跳转指令的分支profile,以及类型相关指令的类型profile。在部分极端情况下,JVM也会在解释执行过程中收集这些profile。

基于分支profile的优化以及基于类型profile的优化都将对程序今后的执行作出假设。这些假设将精简所要编译的代码的控制流以及数据流。在假设失败的情况下,JVM将采取去优化,退回至解释执行并重新收集相关的profile。

使用参数-XX:CompileCommand=’print,*ClassName.methodName’可以打印程序运行过程中即时编译器生成的机器码。