一、即时编译
1.1 分层编译
1.1.1 现象
先执行下面的代码,代码很简单,只是记录下 200 次创建 1000 个对象耗费的时间
public class JIT1 {
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));
}
}
}
每次打印的时间如下所示
可以看到,前期打印的时间比较长,而后期打印所花费的时间比较短,为什么会出现这种情况呢?
1.1.2 解释
其实在运行期间,Java 虚拟机会对我们的代码进行一些优化。上面出现这种现象的原因 JVM 将执行状态分成了 5 个层次,如下
1、0 层,解释器执行(Interpreter),当字节码被反复调用,当反复调用的次数达到阈值时,就会用编译器对字节码进行编译执行,即从 0 层上升到 1 层。
2、1 层,使用 C1 即时编译器编译执行(不带 profiling)
3、2 层,使用 C1 即时编译器编译执行(带基本的 profiling)
4、3 层,使用 C1 即时编译器编译执行(带完全的 profiling)
5、4 层,使用 C2 即时编译器编译执行。
profiling 是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的回边次数】等。
即使编译器分为 C1 和 C2 两种,C1 只是做了一些基本的优化,C2 做了一些更完全更彻底的优化。
1.1.3 即时编译器(JIT)与解释器的区别
1、解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释。
2、JIT 是将一些反复执行的代码编译为机器码,并存入 Code Cache,下次遇到相同的代码,直接执行,无需再编译。
3、解释器是将字节码解释为针对所有平台都通用的机器码。
4、JIT 会根据平台类型,生成平台特定的机器码。
1.1.4 总结
对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行。另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。 执行效率上简单比较一下 Interpreter < C1 < C2,总的目标是发现热点代码(hotspot名称的由来),加以优化。
刚才的一种优化手段称之为【逃逸分析】,它会分析你新创建的 new Object() 对象会不会被循环外的代码用到,发现不会,即不会发生逃逸,逃逸分析是发生在 C2 编译器里面的优化,它就会把你创建对象的字节码给替换掉,即不会创建对象了,所以速度一下子提升了很多。
可以使用 -XX:-DoEscapeAnalysis 关闭逃逸分析,再运行刚才的示例观察结果,就会发现时间不会缩短很多了。
1.2 方法内联
先看下下面的代码
private static int square(final int i) {
return i * i;
}
System.out.println(square(9));
JVM 如果发现 square 是热点方法,并且长度不太长时,会进行内联,所谓的内联就是把方法内代码拷贝、粘贴到调用者的位置,如下
System.out.println(9 * 9);
还能够进行常量折叠(constant folding)的优化,如下
System.out.println(81);
接下来我们试验下 JVM 是否存在这种优化,运行下面的代码
public class JIT2 {
// -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining (解锁隐藏参数)打印 inlining 信息
// -XX:CompileCommand=dontinline,*JIT2.square 禁止某个方法 inlining
// -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("%d\t%d\t%d\n",i,x,(end - start));
}
}
private static int square(final int i) {
return i * i;
}
}
可以看到,当打印时间为 0 时就进行了方法内联,此时根本没有发生方法的调用,可以通过下面的这些参数对内联情况进行控制,如下
# (解锁隐藏参数)打印 inlining 信息
-XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining
# 禁止某个方法 inlining
-XX:CompileCommand=dontinline,*JIT2.square
# 打印编译信息
-XX:+PrintCompilation
二、反射优化
先看下下面的代码
public class Reflect1 {
public static void foo() {
System.out.println("foo...");
}
public static void main(String[] args) throws Exception {
Method foo = Reflect1.class.getMethod("foo");
for (int i = 0; i <= 16; i++) {
System.out.printf("%d\t", i);
foo.invoke(null);
}
System.in.read();
}
}
foo.invoke() 方法前面 0 ~ 15 次调用使用的是 MethodAccessor 的 NativeMethodAccessorImpl 实现。
当调用到第 16 次(从 0 开始算)时,会采用运行时生成的类代替掉最初的实现,可以通过 debug 得到类名为 sun.reflect.GeneratedMethodAccessor1