分层编译

分层编译

随着代码的执行,JVMJIT编译器会将部分热点代码编译为目标机器代码;JVM提供了一个参数-Xcomp,这个参数可以使JVM运行在纯编译的模式,所有的方法在第一次调用的时候就会编成机器代码,但是设置了这个参数之后系统启动负载的确没有上升,但是启动的时间是原来的两倍多。

除了纯编译和默认的mixed之外,JVMjdk6u25之后,引入了分层编译(-XX:+TieredCompilation)HotSpot内置两种编译器,分别是client启动时的c1编译器和server启动时的c2编译器,c2在将代码编译成机器代码的时候需要搜集大量的统计信息以便在编译的时候进行优化,因此编译出来的代码执行效率比较高,代价是程序启动时间比较长,而且需要执行比较长的时间,才能达到最高性能;与之相反,c1的目标是使程序尽快进入编译执行的阶段,所以在编译前需要搜集的信息比c2要少,编译速度因此提高很多,但是付出的代价是编译之后的代码执行效率比较低,但尽管如此,c1编译出来的代码在性能上比解释执行的性能已经有很大的提升,所以所谓的分层编译,就是一种折中方式,在系统执行初期,执行频率比较高的代码先被c1编译器编译,以便尽快进入编译执行,然后随着时间的推移,执行频率较高的代码再被c2编译器编译,以达到最高的性能。

// globalDefinitions.hpp
enum CompLevel {
  CompLevel_any               = -1,
  CompLevel_all                = -1,
  CompLevel_none               = 0,         // Interpreter
  CompLevel_simple             = 1,    // C1
  CompLevel_limited_profile  = 2,   // C1, invocation & backedge counters
  CompLevel_full_profile      = 3,   // C1, invocation & backedge counters + mdo
  CompLevel_full_optimization = 4,  // C2 or Shark
  ... ...
};

目前分层编译包括五个编译层次:Level 0 - Level 4。大家可以在启动参数中加入-XX:+PrintCompilation来查看被编译的方法及其层次:

  • Level0即解释执行,由解释器负责执行java方法。这种方式无编译开销,但速度很慢;解释器并不开启性能监控功能,可触发第一层编译。
  • Level1程序由C1编译器编译为本地机器指令执行,C1编译器会对字节码进行简单和可靠的优化,以达到更快的编译速度。编译方法受限,编译开销比较低,性能比Level4差,但强于其他层次。
  • Level2程序由C1编译器编译为本地机器指令执行,但C2编译器会启动一些编译耗时更长的优化(代码将有可能被重复编译多次,甚至有可能根据性能监控信息进行一些不可靠的激进优化。编译开销比较低,性能差于Level4Level1
  • Level3C1负责编译,除了方法执行次数和回边次数的统计外,还加入了对方法内部执行信息的统计,如一个分枝是否执行跳转,一个虚函数调用最终调用到哪个方法等信息。Level3性能较差,仅比Level0快,但是Level3Level4编译的必要步骤。程序由C1编译器编译为本地机器指令执行,采集性能数据进行优化措施;
  • Level4C2负责编译,它利用level3收集的信息,对方法进行完全的优化,性能最好,但是编译开销也最大。程序由C2编译器编译为本地机器指令执行,进行完全优化。

下述列举了判断一个方法是否需要触发编译的及编译到哪个层次的具体公式:

参数 说明 默认值
Tier3InvocationThreshold 一个方法被调用多少次之后会进行level3编译 200
Tier4InvocationThreshold 一个方法被调用多少次之后会进行level4编译 5000
Tier3CompileThreshold 考虑回边的情况下,一个方法执行多少次之后会进行level3编译 2000
Tier4CompileThreshold 考虑回边的情况下,一个方法执行多少次之后会进行level4编译 15000
Tier3BackEdgeThreshold 一个方法中回边执行多少次会进行OSR level3编译 60000
Tier4BackEdgeThreshold 一个方法中回边执行多少次会进行OSR level4编译 40000
CICompilerCount 编译线程数目
Tier3LoadFeedback 用来动态调整level3编译阈值的值 5
Tier4LoadFeedback 用来动态调整level4编译阈值的值 3
Tier3DelayOn 平均每个C2编译线程排队个数达到多少个时停止level3编译 5
Tier3DelayOff 平均每个C2编译线程排队个数降低到多少个时恢复level3编译 2

最常见的0 -> 3 -> 4编译,高C2编译压力下的0 -> 2 -> 3 -> 4编译;level2的性能比level3好。当C2的编译压力很大的情况下,新编译的level3方法可能方法会长时间的运行在效率相对比较低的代码上而不能及时晋升到level4。在这种情况下,将待编译的方法编译成level2而不是level3会是一个更好的方法。总体上,C2编译压力比较大的情况下是先解释执行,达到一定次数编译成level2,然后等C2编译压力变小的时候会晋升成level3,最后如果执行次数足够多的话会晋升成level4

Level1level2level3不同,它是一个不收集运行数据的最终编译状态。 当一个方法需要进行编译的时候,我们会首先判断它是否符合以下条件:

  • 这个方法只是用来获取类中的某个域的值的
  • 这个方法只是用来获取常量的
  • 这个方法很小

满足这三个条件其中之一的方法会直接被编译成level1并且不会晋升为其他level。由于C2编译耗时较多,往往要达到几百毫秒甚至超过一秒,因此在高峰期行Level4编译通常并不划算,因此JVM参数的调整的思路是增大level4阈值,减少level4编译。具体的,可以通过增大Tier4InvocationThresholdTier4CompileThreshold来增大阈值。编译开销主要在于C2的编译线程,因此通过一定的手段限制C2编译线程的CPU使用可以减少高峰期编译开销。在本次测试中,使用在每编译完成一个方法后,sleep 200ms的方法来限制C2编译线程CPU使用率。

下一页