Table of Contents

  1. HotSpot 内的即时编译器
    1. 解释器和编译器各有各的优点
    2. 被编译对象和触发条件
  2. 编译过程
    1. 编译期优化(早期优化)
    2. 运行时优化(晚期优化)

在部分的商用虚拟机中,Java 程序最初是通过解释器(Interpreter )进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁的时候,就会把这些代码认定为“热点代码”。为了提高热点代码的执行效率,在运行时,即时编译器(Just In Time Compiler )会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化。

HotSpot 内的即时编译器

解释器和编译器各有各的优点

  • 解释器优点:当程序需要迅速启动的时候,解释器可以首先发挥作用,省去了编译的时间,立即执行。解释执行占用更小的内存空间。同时,当编译器进行的激进优化失败的时候,还可以进行逆优化来恢复到解释执行的状态。

  • 编译器优点:在程序运行时,随着时间的推移,编译器逐渐发挥作用根据热点探测功能,,将有价值的字节码编译为本地机器指令,以换取更高的程序执行效率。
    因此,整个虚拟机执行架构中,解释器与编译器经常配合工作。

HotSpot中内置了两个即时编译器,分别称为 Client Compiler和 Server Compiler ,或者简称为 C1编译器和 C2编译器。

目前的 HotSpot编译器默认的是解释器和其中一个即时编译器配合的方式工作,具体是哪一个编译器,取决于虚拟机运行的模式,HotSpot虚拟机会根据自身版本与计算机的硬件性能自动选择运行模式

用户也可以使用 -client和 -server参数强制指定虚拟机运行在 Client模式或者 Server模式。这种配合使用的方式称为“混合模式”(Mixed Mode)

用户可以使用参数 -Xint 强制虚拟机运行于 “解释模式”(Interpreted Mode),这时候编译器完全不介入工作。

另外,使用 -Xcomp 强制虚拟机运行于 “编译模式”(Compiled Mode),这时候将优先采用编译方式执行,但是解释器仍然要在编译无法进行的情况下接入执行过程。通过虚拟机 -version 命令可以查看当前默认的运行模式。

被编译对象和触发条件

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

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

对于第一种,编译器会将整个方法作为编译对象,这也是标准的JIT 编译方式。

对于第二种是由循环体出发的,但是编译器依然会以整个方法作为编译对象,因为发生在方法执行过程中,称为栈上替换。

判断一段代码是否是热点代码,是不是需要出发即时编译,这样的行为称为热点探测(Hot Spot Detection),探测算法有两种,分别为。

  • 基于采样的热点探测(Sample Based Hot Spot Detection):虚拟机会周期的对各个线程栈顶进行检查,如果某些方法经常出现在栈顶,这个方法就是“热点方法”。好处是实现简单、高效,很容易获取方法调用关系。缺点是很难确认方法的reduce,容易受到线程阻塞或其他外因扰乱。
  • 基于计数器的热点探测(Counter Based Hot Spot Detection):为每个方法(甚至是代码块)建立计数器,执行次数超过阈值就认为是“热点方法”。优点是统计结果精确严谨。缺点是实现麻烦,不能直接获取方法的调用关系。
    HotSpot 使用的是第二种-基于技术其的热点探测,并且有两类计数器:方法调用计数器(Invocation Counter)和回边计数器(Back Edge Counter)。
    这两个计数器都有一个确定的阈值,超过后便会触发 JIT 编译。

编译过程

编译期优化(早期优化)

为了保证JRuby,Groovy等语言编译的字节码也能得到性能优化,JVM将性能优化放在了后期的运行时优化,即JIT运行时编译优化中。
具体优化:

  1. 编译期优化主要为语法糖,用来实现Java的各种新的语法特性,比如泛型,变长参数,自动装箱/拆箱。
  2. Java语法糖:与字节码无关,编译后会去掉它们。作用仅仅为方便码农写代码,以及将运行时异常在编译期及早发现(如泛型的使用)。
  3. 泛型与类型擦除
    Java泛型只在编译期存在,编译完成后的字节码中会替换为原生类型。故称Java泛型为伪泛型。C#的泛型在运行期仍然存在。
  4. 条件编译
    if语句中使用常量。比如if(false) {},这个语句块不会被编译到字节码中.这个过程在编译时的控制流分析中完成。

运行时优化(晚期优化)

不同JVM的运行时优化策略
Hotspot采用解释器与编译器并存的构架。
第0层,解释执行,不开启性能监控器,可触发第一层编译
第1层,将字节码编译为机器码,进行简单可靠的优化,可以开启性能监控
第2层,将字节码编译为机器码,会开启一些编译耗时的优化和一些不可靠的激进

具体优化

  • 公共字表达式消除
    如果一个表达式E已经被计算过了,并且从先前的计算到现在E中所有变量的值都没有发生变化,那么E的这次出现就称为了公共子表达式。对于这种表达式,没有必要花时间再对它进行计算,只需要直接用前面计算过的表达式结果代替E就可以了。

  • 数组边界检查消除
    数组边界检查消除(Array Bounds Checking Elimination)是即时编译器中的一项语言相关的经典优化技术。Java访问数组的时候系统将会自动进行上下界的范围检查,但对于虚拟机的执行子 系统来说,每次数组元素的读写都带有一次隐含的条件判定操作,对于拥有大量数组访问的程序代码,这无疑也是一种性能负担。
    数组边界检查时必须做的,但数组边界检查在某些情况下可以简化。例如数组下标示一个常量,如foo3,只要在编译器根据数据流分析来确定foo.length的值,并判断下标“3”没有越界,执行的时候就无须判断了。再例如数组访问发生在循环之中,并且使用循环变量来进行数组访问,如果编译器只要通过数据流分析就可以判定循环变量的取值范围永远在区间[0, foo.length)之内,那在整个循环中就可以把数组的上下界检查消除掉,这可以节省很多次的条件判断操作。
    与语言相关的其他消除操作还有自动装箱消除(Autobox Elimination)、安全点消除(Safepoint Elimination)、消除反射(Dereflection)等。

  • 方法内联
    方法内联是编译器最重要的优化手段之一,除了消除方法调用的成本之外,更重要的是可以为其他优化手段建立良好的基础。
    逃逸分析
    逃逸分析(Escape Analysis)并不是直接优化代码的手段,而是为其他优化手段提供依据的分析技术。

参考: