1. Java 语言编译过程中比较有代表性的编译器:
1) 前端编译器:把 *.java 文件转变成 *.class 文件的过程,如 Sun 的 Javac 、 Eclipse JDT 中得增量式编译器。
2) JIT 编译器:虚拟机的后端运行期编译器把字节码转化成机器码的过程,如 HotSpot VM 的 C1 、 C2 编译器。
3) AOT 编译器:静态提前编译器直接把 *.java 文件编译成本地机器代码的过程,如 GUN Compiler for the Java ( GCT )、 Excelsior JET 。
2. 在部分的商用虚拟机( Sun Hotspot 、 IBM J9 )中, Java 程序最初是通过解释器进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁,就会把这些代码认定为“热点代码”,为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器。
3. 当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。当程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获取更高的执行效率。当程序运行环境中内存资源限制较大,可以使用解释执行节约内存,反之可以使用编译执行来提升效率。
4. Hotspot 虚拟机中内置了两个即时编译器,称为 Client Compiler 和 Server Compiler ,或者简称 C1 编译器和 C2 编译器。
5. 基于计数器的热点探测:采用这种方法的虚拟机会为每个方法建立计数器,统计方法的执行次数,如果执行次数超过一定的阀值就认为它是“热点方法”。
6. 在 HotSpot 虚拟机中使用的是基于计数器的热点探测方法,因此它为每个方法准备了两个计数器:方法调用计数器和回边计数器。
7. 方法调用计数器:用于统计方法别调用的次数,默认阀值在 Client 模式下是 1500 次,在 Server 模式下 10000 次,这个阀值可以通过虚拟机参数 -XX:CompileThreshold 来人工设定。
8. 当超过一定的时间限度,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那这个方法的调用计数器就会减少一半,这个过程称为方法调用计数器的热度衰减,而这段时间就称为半衰周期,可以使用虚拟机参数 -XX:UseCounterDecay 来关闭热度衰减,另外可以使用 -XX:CounterHalfLifeTime 参数设置半衰周期的时间,单位是秒。
9. 回边计数器:用于统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令就称为“回边”。回边计数器没有计数热度衰减的过程,因此这个计数器统计的就是该方法循环执行的绝对次数。
10. Client Compiler 又称为 C1 ,只做少量性能开销比高的优化,它占用内存较少,适合于桌面交互式应用。在寄存器分配策略上, JDK6 以后采用的为线性扫描寄存器分配算法,在其他方面的优化主要有:方法内联、去虚拟化、冗余消除等。
1) 方法内联:对于 Java 类面向对象的语言,通常要调用多个方法来完成功能。执行时,要经历多次参数传递、返回值传递及跳转等,于是 C1 采取了方法内联的方式,即把调用到的方法的指令直接植入当前方法中。小于 -XX:MaxInlineSize=35 字节的函数指令直接植入 -XX:+PrintInlining 可以查看内联信息;如:
bar(){ small(); } small(){ ...Code; } -> bar(){ ...Code; }
2) 去虚拟化:去虚拟化是指在装载 class 文件后,进行类层次的分析,如发现类中的方法只提供一个实现类,那么对于调用了此方法的代码,也可进行方法内联,从而提升执行的性能。接口只有一个实现类时,直接植入实现方法指令;如:
execute(IFoo foo){foo.bar(Code)} ->execute(){Code}
3) 冗余消除:根据运行时状况进行代码折叠或消除,如:
if(true){Code}->Code
11. Server Compiler 又称为 C2 , C2 采用了大量的传统编译优化技巧来进行优化,占用内存相对会多些,适合与服务器端的应用。寄存器分配策略上 C2 采用的为传统的图着色寄存器分配算法,由于 C2 会收集程序的运行信息,因此其优化的范围更多在于全局的优化。收集的信息主要有:分支的跳转 / 不跳转的频率、某条指令上出现过的类型、是否出现过空值、是否出现过异常。
1) 逃逸分析:当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,这种行为称为方法逃逸。甚至还有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,这种行为称为线程逃逸。用户可以使用参数 -XX:+DoEscapeAnalysis 来手动开启逃逸分析。
2) 栈上分配:如果确定一个对象不会逃逸出方法之外,那让这个对象在栈上分配内存将会是一个很不错的主意,对象所占用的内存空间就可以随栈帧出栈而销毁。
3) 同步消除:线程同步本身就是一个相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那这个变量的读写肯定就不会有竞争,对这个变量实施的同步措施也就可以消除掉。
4) 标量替换:如果把一个 Java 对象拆散,根据程序访问的情况,将其使用到的成员变量恢复原始类型来访问就叫做标量替换。如果逃逸分析证明一个对象不会被外部访问,并且这个对象可以被拆散的话,那程序真正执行的时候将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替。
出了 C1 、 C2 外,还有一种较为特殊的编译为: OSR ( On Stack Replace )。 OSR 编译和 C1 、 C2 最主要的不同点在于 OSR 编译只替换循环代码的入口,而 C1 、 C2 替换的是方法调用的入口,因此在 OSR 编译后会出现的现象是方法的整段代码被编译了,但只有在循环代码体部分才执行编译后的机器码,其他部分则仍然是解释执行方式。