JIT编译器介绍

深入学习Java基础知识
2021-05-17 14:29 · 阅读时长4分钟
小课

JIT即Just In Time的缩写,JIT编译器它是一个能够在运行时将字节码编译转化为机器码的编译器,它的主要目的是为了在运行时提升性能。JIT编译器有两种,Client Compiler和Server Compiler,它们分别适用不同的场景。

Client Compiler

比较有名的就是C1编译器,适用于执行时间短,但是对启动时间比较敏感的应用,它的启动速度快,占用内存少,但是优化效果相对于Server Compiler会差一点。

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的工作量。

jvmjitjit compiler