日常的所谓的Java编译, 一般指的是前端编译, 也就是javac编译, 将写的.java文件, 编译成.class文件, 这个过程里javac并不做性能优化, 只会做语法检查. 而相应的后端编译, 指的就是将字节码转换成在操作系统执行的机器码的过程. 这个过程中, Java采用的混合的办法, 包括解释执行和编译执行, 其中的编译执行, 就是所谓的即使编译器的作用. 这篇文章下面所指的编译, 主要就是指这个过程的编译.
分层编译模式
Hotspot虚拟机有多个即时编译器, C1(Client Compiler)和C2(Server Compiler). 当然最新的还有Graal即时编译器.
Java7之前, 对于启动性有较高要求的, 我们选择C1, 对应的参数为 -client. 对于性能要求比较高的, 使用 -server 来启动C2编译器.
Java7开始, 引入了分层编译的概念,对应参数-XX:+TieredCompilation
, 它综合了C1的启动优势和C2巅峰性能优势. 分层编译将jvm的执行状态分为五个层次:
- 解释执行
- 执行不带profiling(例如JDK自带的hprof)的C1代码(C1生成的机器码)
- 执行部分profiling的C1代码
- 执行所有profiling的C1代码
- 执行C2代码
一般C2代码的执行效率要高于C1 30%以上. 上面C1的三个层次, 当然是越来越慢的. 热点的代码会被C1编译, 然后再被C4编译.
Java8是默认开启了分层编译, 原来的参数-client 和 -server参数都无效, 如果关闭分层编译, jvm将直接采用C2. 如果你希望只用C1, 那么在开启分层编译时可以用参数-XX:TieredStopAtLevel=1
, 这时,在解释执行后,直接由C1进行编译.
即时编译的触发
jvm是根据方法的调用次数和循环回边的执行次数来触发即时编译的. 那么什么是循环回边? 例如下面的代码:
public static void foo(Object obj) {
int sum = 0;
for (int i = 0; i < 10; i++) {
sum += i;
}
}
代码在执行时, 循环会一次次执行, 每结束一次循环, 回到下一次循环的起点继续执行, 而循环的两个边界, 就是循环回边. 在解释执行时, 每运行一次, jvm就会将该方法的循环回边计数器 +1. 如果是C1, 它也是在这加循环回边计数器的代码. 这个计数器是不做同步的, 所以数值不一定准确, 但是可以确定的是足够大就可以说明他是热点方法了.
当不启用分层编译的情况下, 方法的调用次数 + 循环回边的次数和超过-XX:CompileThreshold
指定的值时, 就会触发即时编译(C1默认1500,C2默认10000).
当启动分层编译时, 参数-XX:CompileThreshold
将会失效, 阀值的大小是动态的.