public class Test {
// -XX:+PrintCompilation 会打印出java文件编译后的样子 -XX:-DoEscapeAnalysis 是否进行逃逸分析,“-”就是关掉。
public static void main(String[] args) {
for (int i = 0; i < 200; i ++) {
long start = System.nanoTime();
for(int j = 0; j < 1000; j ++) {
new Object();
}
long end = System.nanoTime();
System.out.printf("%d\t%d\n", i, (end - start));
}
}
}
这个例子分了两层循环,内层循环创建1000个Object对象,外层循环对内层循环进行计时统计,看每次创建1000个对象耗费多长时间。运行如下:
0 66560
1 49066
2 45227
3 49067
4 58880
5 58880
6 55040
7 52053
8 49920
9 46933
10 45226
...
68 18774
69 18346
...
145 61867
146 853
147 854
...
前面的是外层循环的次数,后面的数字是内层循环创建1000个对象耗费的时间。可以看到前面的有些循环是5开头甚至6开头,大约在第67次循环时,这个速度好像更快了,以2或1开头了,大约到146次时,速度一下子降到了三位数。这是因为,在运行期间,Java虚拟机会对我们的代码做一定的优化。
实际上,JVM把字节码执行的执行状态
分成了5个层次:
即时编译器(JIT)与解释器的区别:
因为代码都运行起来了,所以肯定确定了平台,所以他会进行更激进的更彻底的优化
)需要注意的是,其实有的时候很多代码或只会调用一次,很多方法也只会调用一次,那如果把这些每次编译成机器码,其实这个编译本身也是要耗费时间的,所以JVM采用的是接下来的策略:对于占据大部分的不常用的代码,没必要耗费时间将其编译成机器码,因为这些代码执行的频率很少,所以采取解释执行的方式运行;另一方面,如果对于仅占据小部分的热点代码,比如方法调用次数或循环次数达到一定的阈值,我们则可以将其编译成机器码,以达到比较理想的运行速度。
运行效率上简单比较一下,Interpreter < C1 < C2
,C1的效率可以提升到五倍左右,C2的执行效率可以提升到10~100倍,所以即时编译器总的目标就是去发现热点代码(这也是为什么oracle的虚拟机叫hotspot,hotspot就是热点的意思
),并加以优化。
上面例子中使用的优化手段叫“逃逸分析
”,所谓逃逸分析就是比如在上面代码中,他去分析new Object对象会不会在循环外面会用到,或会不会被其他方法所引用,结果发现不会(本例子中
),因为它就是循环内的局部变量,所以他就采用了这种优化手段,即他发现创建对象的操作不会逃逸,即意味着外层不会用到这个对象,既然不会用到我干嘛创建你呢,所以可以看到刚才他的执行时间突然变短的原因是因为JIT进行了逃逸分析以后,当然这个逃逸分析是在C2编译器里做的优化,会把这个对象的创建的字节码干脆给你替换掉,也就是说C2编译器生成的机器码他已经可以把你原本的字节码改的面目全非,就干脆不会创建对象了(本例子
),所以速度一下子提升了这么多。这种优化手段叫逃逸分析。
如果在上面代码中关掉逃逸分析(-XX:-DoEscapeAnalysis
),那么加上这个虚拟机参数后运行的话,可以看到这200次循环的时间后面虽然比前面也少了点,但不会出现明显的变成3位数等现象,这说明Object对象仍然是被创建。即关闭逃逸分析的话,他就没办法进入最终的C2编译器阶段了。
方法内联也是即时编译器的优化手段的一种,比如下面的例子:
private static int square(final int i) {
return i * i;
}
该例子是求平方的函数,内部针对整数i做了平方。其实,我们去调用这个函数时:
System.out.println(square(9));
比如,他认为这个函数如果比较短,而且这种函数如果是热点代码时,那么他就会做一个叫“方法内联
”。方法内联非常简单,他就是把函数内的代码拷贝到他的调用者的位置,即进行方法内联以后变成下面的样子:
System.out.println(9 * 9);
就是相当于把方法内的代码给他拿出来,放在方法调用者这里。并且他还能再做一个进一步的优化,因为每次调用的这个值(9
)固定不变,就可以认为他是个常量,所以还可以做一个叫“常量折叠(constant folding)
”的优化,直接把他算出来的结果就传给System.out.println:
System.out.println(81);
以上是方法内联的优化策略。下面通过实验去观察方法内联的效率上的提升。
public class Test {
// -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining -XX:CompileCommand=dontinline,*JIT2.square
// -XX:+PrintCompilation
public static void main(String[] args) {
int x = 0;
for (int i = 0; i < 500; i ++) {
long start = System.nanoTime();
for (int j = 0; j < 1000; j ++) {
x = square(9);
}
long end = System.nanoTime();
System.out.printf(