即时编译与优化技术

Java程序最初是仅仅通过解释器解释执行的,即对字节码逐条解释执行,这种方式的执行速度相对会比较慢,尤其当某个方法或代码块运行的特别频繁时,这种方式的执行效率就显得很低。于是后来在虚拟机中引入了JIT编译器(即时编译器),当虚拟机发现某个方法或代码块运行特别频繁时,就会把这些代码认定为“Hot Spot Code”(热点代码),为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各层次的优化,完成这项任务的正是JIT编译器。
为了在程序启动响应速度和运行效率之间达到最佳平衡,从虚拟机采用了分次编译策略。分分层编译根据编译器编译、优化的规模与耗时,划分出不同的编译层次。其中包括:

  • 解释执行,解释执行不开启性能监控功能,可触发第一层编译
  • C1编译,将字节码编译为本地代码,进行简单、可靠的优化,如有必要加入性能监控的逻辑。
  • C2编译,将字节码便以为本地代码,会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。

运行过程中会被即时编译器编译的“热点代码”有两类:

  • 被多次调用的方法。
  • 被多次调用的循环体。

以上两种情况,编译器都是以整个方法作为编译对象,这种编译也是虚拟机中标准的编译方式。要知道一段代码或方法是不是热点代码,是不是需要触发即时编译,需要进行Hot Spot Detection(热点探测)。目前主要的热点 判定方式有以下两种:

  • 基于采样的热点探测:采用这种方法的虚拟机会周期性地检查各个线程的栈顶,如果发现某些方法经常出现在栈顶,那这段方法代码就是“热点代码”。这种探测方法的好处是实现简单高效,还可以很容易地获取方法调用关系,缺点是很难精确地确认一个方法的热度,容易因为受到线程阻塞或别的外界因素的影响而扰乱热点探测。
  • 基于计数器的热点探测:采用这种方法的虚拟机会为每个方法,甚至是代码块建立计数器,统计方法的执行次数,如果执行次数超过一定的阀值,就认为它是“热点方法”。这种统计方法实现复杂一些,需要为每个方法建立并维护计数器,而且不能直接获取到方法的调用关系,但是它的统计结果相对更加精确严谨。
    在HotSpot虚拟机中使用的是第二种——基于计数器的热点探测方法,因此它为每个方法准备了两个计数器:方法调用计数器和回边计数器。
  • 方法调用计数器用来统计方法调用的次数,在默认设置下,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间内方法被调用的次数。
  • 回边计数器用于统计一个方法中循环体代码执行的次数(准确地说,应该是回边的次数,因为并非所有的循环都是回边),在字节码中遇到控制流向后跳转的指令就称为“回边”。
    在确定虚拟机运行参数的前提下,这两个计数器都有一个确定的阀值,当计数器的值超过了阀值,就会触发JIT编译。触发了JIT编译后,在默认设置下,执行引擎并不会同步等待编译请求完成,而是继续进入解释器按照解释方式执行字节码,直到提交的请求被编译器编译完成为止(编译工作在后台线程中进行)。当编译工作完成后,下一次调用该方法或代码时,就会使用已编译的版本。
    方法调用计数器触发及时编译如下图所示:

编译优化技术

  1. 公共子表达式消除:如果一个表达式中E已经计算过,并且E中所有变量值都没有变化,那么下次可以直接使用已经计算过的E值。
  2. 数组边界检查消除:如果在编译期间已经判断过数组越界,在执行期间就不需要再次判断。
  3. 方法内联:把目标方法复制到发起调用的方法之中
  4. 逃逸分析:当一个对象在方法中被定义后,他可能被外部方法所引用,例如作为调用参数传递到其他方法中,称为方法逃逸。甚至还有可能被外部线程访问到,称为线程 逃逸。如果能证明一个对象不会套移到方法或线程之外,也就是别的方法或线程无法通过任何途径访问到这个对象,则可以采用如下手段进行优化。
  • 栈上分配对象:如果一个对象不会逃出方法之外,那么可以根据情况将这个对象直接分配在栈上。
  • 同步消除:如果变量不会逃出线程,无法被其他线程方法,可以去除对该变量的同步。
  • 标量替换:如果一个对象不会被外部访问,并且这个对象可以被拆散的话,程序可以不创建这个对象,而改为直接创建他的若干个被这个方法使用到的成员变量来代替。
10-02 16:57