Java 中即时编译器在运行期的优化过程对于程序运行来说更重要,而前端编译器在编译期的优化过程对于程序编码来说更加密切
Java 语言的「编译期」其实是一段「不确定」的操作过程。因为它可能是一个前端编译器(如 Javac)把 *.java 文件编译成 *.class 文件的过程;也可能是程序运行期的即时编译器(JIT 编译器,Just In Time Compiler)把字节码文件编译成机器码的过程;还可能是静态提前编译器(AOT 编译器,Ahead Of Time Compiler)直接把 *.java 文件编译成本地机器码的过程。
Javac 编译器的编译过程大致可分为 3 个步骤:

Java 中提供了有很多语法糖来方便程序开发,虽然语法糖不会提供实质性的功能改进,但是它能提升开发效率、语法的严谨性、减少编码出错的机会。下面我们来了解下语法糖背后我们看不见的东西。
其中包括:
在部分商业虚拟机中,Java 最初是通过解释器解释执行的,当虚拟机发现某个方法或者代码块的运行特别频繁时,就会把这些代码认定为「热点代码」(Hot Spot Code)。为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(JIT)。
PS:这两种被多次重复执行的代码,称之为「热点代码」。
由于 Java 虚拟机规范中没有限定即时编译器如何实现,所以本节的内容完全取决于虚拟机的具体实现。我们这里拿 HotSpot 来说明,不过后面的内容涉及具体实现细节的内容很少,主流虚拟机中 JIT 的实现又有颇多相似之处,因此对理解其它虚拟机的实现也有很高的参考价值。
尽管并不是所有的 Java 虚拟机都采用解释器与编译器并存的架构,但许多主流的商用虚拟机,如 HotSpot、J9 等,都同时包含解释器与编译器。解释器与编译器两者各有优势:
同时,解释器还可以作为编译器激进优化时的一个「逃生门」,当编译器根据概率选择一些大多数时候都能提升运行速度的优化手段,当激进优化的假设不成立,如加载了新的类后类型继承结构出现变化、出现「罕见陷阱」时可以通过逆优化退回到解释状态继续执行。
判断一段代码是不是热点代码,是不是需要触发即时编译,这样的行为称为==「热点探测」==。其实进行热点探测并不一定需要知道方法具体被调用了多少次,目前主要的热点探测判定方式有两种:
HotSpot 虚拟机采用的是第二种:基于计数器的热点探测。因此它为每个方法准备了两类计数器:方法调用计数器(Invocation Counter)和回边计数器(Back Edge Counter)。
顾名思义,这个计数器用于统计方法被调用的次数。当一个方法被调用时,会首先检查该方法是否存在被 JIT 编译过的版本,如果存在,则优先使用编译后的本地代码来执行。如果不存在,则将此方法的调用计数器加 1,然后判断方法调用计数器与回边计数器之和是否超过方法调用计数器的阈值。如果超过阈值,将会向即时编译器提交一个该方法的代码编译请求。

回边计数器的作用是统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为「回边」(Back Edge)。建立回边计数器统计的目的是为了触发 OSR 编译。

我们都知道,以编译方式执行本地代码比解释执行方式更快,一方面是因为节约了虚拟机解释执行字节码额外消耗的时间;另一方面是因为虚拟机设计团队几乎把所有对代码的优化措施都集中到了即时编译器中,所以这一小节我们来介绍下 HotSpot 虚拟机的即时编译器在编译代码时采用的优化技术。
这段代码看起来简单,但是有许多可以优化的地方:

第一步是进行方法内联(Method Inlining),方法内联的重要性要高于其它优化措施。因此,各种编译器一般都会把内联优化放在优化序列的最前面。内联优化后的代码如下:

第二步进行冗余消除,代码中「z = b.value;」可以被替换成「z = y」。这样就不用再去访问对象 b 的局部变量。如果把 b.value 看做是一个表达式,那也可以把这项优化工作看成是公共子表达式消除。优化后的代码如下:

第三步进行复写传播,因为这段代码里没有必要使用一个额外的变量 z,它与变量 y 是完全等价的,因此可以使用 y 来代替 z。复写传播后的代码如下:

第四步进行无用代码消除。无用代码可能是永远不会执行的代码,也可能是完全没有意义的代码。因此,又被形象的成为「Dead Code」。上述代码中 y = y 是没有意义的,因此进行无用代码消除后的代码是这样的:

经过这四次优化后,最新优化后的代码和优化前的代码所达到的效果是一致的,但是优化后的代码执行效率会更高。
编译器的这些优化技术实现起来是很复杂的,但是想要理解它们还是很容易的。接下来我们再讲讲如下几项最有代表性的优化技术是如何运作的,它们分别是:
公共子表达式消除
如果一个表达式 E 已经计算过了,并且从先前的计算到现在 E 中所有变量的值都没有发生变化,那么 E 的这次出现就成了公共子表达式。对于这种表达式,没有必要花时间再对它进行计算,只需要直接使用前面计算过的表达式结果代替 E 就好了。如果这种优化仅限于程序的基本块内,便称为局部公共子表达式消除,如果这种优化的范围覆盖了多个基本块,那就称为全局公共子表达式消除。
数组边界检查消除
如果有一个数组 array[],在 Java 中访问数组元素 array[i] 的时候,系统会自动进行上下界的范围检查,即检查 i 必须满足 i >= 0 && i < array.length,否则会抛出一个运行时异常:java.lang.ArrayIndexOutOfBoundsException,这就是数组边界检查
方法内联
逃逸分析
逃逸分析不是直接优化代码的手段,而是为其它优化手段提供依据的分析技术。逃逸分析的基本行为就是分析对象的动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其它方法中,称为方法逃逸。甚至还有可能被外部线程访问到,例如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸。