通常情况下,Java程序最初都是被编译为字节码,通过解释器进行解释执行,解释执行能够获得更好的启动时间。某些被频繁执行的方法或者代码块,会被JVM认定为“热点代码”。在运行时JVM会把这些热点代码编译成与本地平台相关的机器码,并且进行各种层次的优化,以提高执行效率。完成这个任务的编译器称为即时编译器(JIT编译器)。
HotSpot的分层编译模式
HotSpot内置了C1编译器和C2编译器。默认情况下,JVM采取解释器和其中一个编译器直接配合的运行模式,编译器的选择,根据自身的版本以及宿主机器的硬件性能自动选择。此外,用户也可以通过JVM参数强制JVM的运行模式。如下表:
参数 | 运行模式 | 说明 |
---|---|---|
-Xint | 解释模式 | 编译器不介入工作,所有代码都使用解释器解释执行 |
-Xcomp | 编译模式 | 优先采用编译方式执行,但是在编译无法进行的情况下,还是会进行解释执行 |
-client | 混合模式(Client模式) | 解释器搭配C1的混合模式,适用于对于执行时间较短的,或者对启动性能有要求的程序 |
-server | 混合模式(Server模式) | 解释器搭配C2的混合模式,适用于执行时间较长的,或者对峰值性能有要求的程序 |
分层编译
Java 7 引入了分层编译(对应参数 -XX:+TieredCompilation)的概念,综合了 C1 的启动性能优势和 C2 的峰值性能优势。
分层编译将 Java 虚拟机的执行状态分为了五个层次。为了方便阐述,我用“C1 代码”来指代由 C1 生成的机器码,“C2 代码”来指代由 C2 生成的机器码。五个层级分别是:
- 解释执行;
- 执行不带 profiling 的 C1 代码;;
- 执行仅带方法调用次数以及循环回边执行次数 profiling 的 C1 代码;
- 执行带所有 profiling 的 C1 代码;
-
执行 C2 代码;
其中 1 层的性能比 2 层的稍微高一些,而 2 层的性能又比 3 层高出 30%。这是因为 profiling 越多,其额外的性能开销越大。
不同层次的编译路径如下:
在 5 个层次的执行状态中,1 层和 4 层为终止状态。当一个方法被终止状态编译过后,如果编译后的代码并没有失效,那么 Java 虚拟机是不会再次发出该方法的编译请求的。
编译对象和触发条件
Java 虚拟机是根据方法的调用次数以及循环回边的执行次数来触发即时编译的。前面提到,Java 虚拟机在 0 层、2 层和 3 层执行状态时进行 profiling,其中就包含方法的调用次数和循环回边的执行次数。
去优化
当激进优化(C2的可能编译优化)的假设不成立,如加载了新类后类型继承结构发生了变化、出现了“罕见陷阱”时可以通过逆优化退回到解释执行的模式。
编译优化技术
HotSpot的官方的编译优化技术列表见这里。下面列举一些最有代表性的优化技术是如何运用的。
公共子表达式消除
数组边界检查消除
例如在循环体内访问数组,如果能够通过数据流分析就可以判断循环变量的取值范围永远在[0,array.length],那在循环体中就可以消除数组的上下界检查。
方法内联
逃逸分析
逃逸分析的基本行为就是分析对象的动态作用域。当一个对象在方法中被定义后,它可能被外部方法所引用(例如作为形参传递到其它方法中去),称为方法逃逸。如果是被外部线程访问到,称为线程逃逸。如果能够证明一个对象不会逃逸到方法或者线程之外,则可能对这个对象进行一些高效的优化:
-
栈上分配
如果能够确定一个对象不会逃逸到方法之外,可以在栈上分配对象的内存,这样对象占用的内存* 空间可以随着栈帧出栈而销毁,减少gc的压力; -
同步消除
如果逃逸分析得出对象不会逃逸到线程之外,那么对象的同步措施可以消除。 -
标量替换
如果逃逸分析证明一个对象不会被外部访问,并且这个对象可以被拆解,那么程序执行的时候可能不创建这个对象,改为在栈上分配这个方法所用到的对象的成员变量。
JIT相关的JVM参数
- -XX:CompileThreshold,方法调用计数器触发JIT编译的阀值
- -XX:BackEdgeThreshold,回边计数器触发OSR编译的阀值
- -XX:-BackgroundCompilation,禁止JIT后台编译