JIT即Just In Time的缩写,JIT编译器它是一个能够在运行时将字节码编译转化为机器码的编译器,它的主要目的是为了在运行时提升性能。JIT编译器有两种,Client Compiler和Server Compiler,它们分别适用不同的场景。
比较有名的就是C1编译器,适用于执行时间短,但是对启动时间比较敏感的应用,它的启动速度快,占用内存少,但是优化效果相对于Server Compiler会差一点。
比较有名的有C2编译器和Graal编译器,适用于执行时间长,但是对性能要求高的应用,它们的启动速度会相对慢一些,但是优化效果好。
在Java 7及以前的版本可以通过参数-server
,-client
指定一种JIT编译器,在Java 7时,引入了分层编译,这种方式综合了 C1 的启动性能优势和 C2 的峰值性能优势,我们也可以通过参数-client
,-server
强制指定虚拟机的即时编译模式。
在 Java8 中,默认开启分层编译,-client
和-server
的设置已经无效了。如果想只开启 C2,可以通过设置参数-XX:-TieredCompilation
,如果只开启C1,可以在打开分层编译的同时设置参数-XX:TieredStopAtLevel=1
。除了这种默认的混合编译模式,我们还可以使用-Xint
参数强制虚拟机运行于只有解释器的编译模式下。我们还可以使用参数-Xcomp
强制虚拟机运行于只有JIT的编译模式下。
判断是一个方法是否需要触发即时编译的技术叫热点探测,目前有两种类型的热点探测技术,基于采样的热点探测和基于计数器的热点探测。
HotSpot虚拟机是使用基于计数器的热点探测,它有两种计数器,方法调用计数器和回边计数器,分别用于探测方法和代码块是否为热点代码。
下面介绍几个JIT编译器常用的编译优化手段,方法内联和栈上分配。
方法调用需要经历压栈和出栈,会产生一定的时间和空间方面的开销,如果调用量比较大,这一部分的优化还是很可观的,方法内联就是将调用的方法中的代码直接放到调用处,这样就少了一次方法调用的开销,比如。
private int add1(int x1, int x2, int x3, int x4) {
return add2(x1, x2) + add2(x3, x4);
}
private int add2(int x1, int x2) {
return x1 + x2;
}
在内联之后,会优化成这样。
private int add1(int x1, int x2, int x3, int x4) {
return x1 + x2+ x3 + x4;
}
但是要注意的是,不是说方法内联就一定好,如果方法体太长,JIT编译器就不会进行内联。
用于判断一个对象是否会被外部方法或者其它线程访问,如果一个对象没有逃逸,那么就可以做以下优化。
当一个对象没有逃逸,说明它只会在当前线程访问,此时就可以移除它的同步锁,比如说StringBuffer的很多方法都是synchronized修饰的,如果知道一个StringBuffer对象没有逃逸,这些同步锁就可以移除。
当一个对象没有逃逸,并且这个对象可以被拆分的话,那么就可以不创建这个对象,而直接创建它的成员变量来代替,这样的好处是可以将对象的成员变量在栈内存中,而不是堆内存中,从而减轻GC的工作量。