JIT概述

即时编译是一项用来提升应用程序运行效率的技术。通常而言,代码会先被 Java 虚拟机解释执行,之后反复执行的热点代码则会被即时编译成为机器码,直接运行在底层硬件之上。

从 Java 8 开始,Java 虚拟机默认采用分层编译的方式。它将执行分为五个层次,分为为 0 层解释执行,1 层执行没有 profiling 的 C1 代码,2 层执行部分 profiling 的 C1 代码,3 层执行全部 profiling 的 C1 代码,和 4 层执行 C2 代码。

通常情况下,方法会首先被解释执行,然后被 3 层的 C1 编译,最后被 4 层的 C2 编译。

即时编译是由方法调用计数器和循环回边计数器触发的。在使用分层编译的情况下,触发编译的阈值是根据当前待编译的方法数目动态调整的。

以使用参数 -XX:+PrintCompilation 来打印你项目中的即时编译情况。

JIT优化

方法内联

  • 在编译过程中,当遇到方法调用时,将目标方法的方法体纳入编译范围之中,并取代原方法调用的优化手段
  • 除了一些强制内联以及强制不内联的规则外,即时编译器会根据方法调用的层数、方法调用指令所在的程序路径的热度、目标方法的调用次数及大小,以及当前 IR 图的大小来决定方法调用能否被内联
  • -XX:CompileCommand 中的 inline 指令指定的方法,以及由 @ForceInline 注解的方法(仅限于 JDK 内部方法),会被强制内联。
  • 由 -XX:CompileCommand 中的 dontinline 指令或 exclude 指令(表示不编译)指定的方法,以及由 @DontInline 注解的方法(仅限于 JDK 内部方法),则始终不会被内联
  • 可以利用虚拟机参数 -XX:+PrintInlining 来打印编译过程中的内联情况

去虚化

  • 完全去虚化通过类型推导或者类层次分析,将虚方法调用转换为直接调用。它的关键在于证明虚方法调用的目标方法是唯一的。
  • 条件去虚化通过向代码中增添类型比较,将虚方法调用转换为一个个的类型测试以及对应该类型的直接调用。它将借助 Java 虚拟机所收集的类型 Profile
  • 减少方法的调用,直接使用变量,避免压栈出栈

intrinsic

  • HotSpot 虚拟机将对标注了@HotSpotIntrinsicCandidate注解的方法的调用,替换为直接使用基于特定 CPU 指令的高效实现。这些方法我们便称之为 intrinsic。
  • intrinsic 的实现有两种。
    • 一是不大常见的桩程序,可以在解释执行或者即时编译生成的代码中使用。
    • 二是特殊的 IR 节点。即时编译器将在方法内联过程中,将对 intrinsic 的调用替换为这些特殊的 IR 节点,并最终生成指定的 CPU 指令。

逃逸分析

  • 判断规则
    • 一是看对象是否被存入堆中
    • 二是看对象是否作为方法调用的调用者或者参数
  • 基于逃逸分析的优化
    • 锁消除
    • 栈上分配:由于实现起来需要更改大量假设了“对象只能堆分配”的代码,因此 HotSpot 虚拟机并没有采用栈上分配,而是使用了标量替换这么一项技术
    • 标量替换:减少对象新建回收的过程

字段访问相关优化

  • 即时编译器将沿着控制流缓存字段存储、读取的值,并在接下来的字段读取操作时直接使用该缓存值。
    • 通过新建一个新的局部变量,缓存从内存中读取的值至局部变量
    • 局部变量保存在栈上
    • 当需要使用该变量时,直接从局部变量加载到操作数栈,而不需要再从内存中读取
    • 对于volitile或加锁的会不进行缓存
  • 这要求生成缓存值的访问以及使用缓存值的读取之间没有方法调用、内存屏障,或者其他可能存储该字段的节点
  • 即时编译器还会优化冗余的字段存储操作。如果一个字段的两次存储之间没有对该字段的读取操作、方法调用以及内存屏障,那么即时编译器可以将第一个冗余的存储操作给消除掉。

死代码消除

死代码消除的两种形式。第一种是局部变量的死存储消除。

第二种则是不可达分支。通过消除不可达分支,即时编译器可以精简数据流,并且减少编译时间以及最终生成机器码的大小。

循环优化

  • 循环无关代码外提将循环中值不变的表达式,或者循环无关检测外提至循环之前,以避免在循环中重复进行冗余计算
  • 循环展开是一种在循环中重复多次迭代,并且相应地减少循环次数的优化方式。它是一种以空间换时间的优化方式,通过增大循环体来获取更多的优化机会。
  • 循环判断外提以及循环剥离

向量化

  • 向量化优化借助的是 CPU 的 SIMD 指令,即通过单条指令控制多组数据的运算。它被称为 CPU 指令级别的并行。
  • HotSpot 虚拟机运用向量化优化的方式有两种。
    • 第一种是使用 HotSpot intrinsic,在调用特定方法的时候替换为使用了 SIMD 指令的高效实现。Intrinsic 属于点覆盖,只有当应用程序明确需要这些 intrinsic 的语义,才能够获得由它带来的性能提升。
    • 第二种是依赖即时编译器进行自动向量化,在循环展开优化之后将不同迭代的运算合并为向量运算。自动向量化的触发条件较为苛刻,因此也无法覆盖大多数用例。

即时编译方法测试

/**
 * -XX:CompileCommand=inline,com/kevin/jit/JITTest::inline -XX:CompileCommand=dontinline,com.kevin.jit.JITTest::dontinline -XX:CompileCommand=exclude,com.kevin.jit.JITTest::exclude
 * -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp -XX:CompileCommand=print,*JITTest.inline
 */
public class JITTest {
    public static void main(String[] args) {
        JITTest test = new JITTest();
        long count = 10000l;
        long lastTime = System.currentTimeMillis();
        System.out.println("start: " + lastTime);
        for (int i = 0; i < count; i++) {
            for (int j = 0; j < count; j++) {
                test.inline(i, j);
            }
        }
        System.out.println("exec inline: " + (System.currentTimeMillis() - lastTime));
        lastTime = System.currentTimeMillis();
        for (int i = 0; i < count; i++) {
            for (int j = 0; j < count; j++) {
                test.dontinline(i, j);
            }
        }
        System.out.println("exec dontinline: " + (System.currentTimeMillis() - lastTime));
        lastTime = System.currentTimeMillis();
        for (int i = 0; i < count; i++) {
            for (int j = 0; j < count; j++) {
                test.exclude(i, j);
            }
        }
        System.out.println("exec exclude: " + (System.currentTimeMillis() - lastTime));
    }
    public long inline(long i, long j) {
        return i * j;
    }
    public long dontinline(long i, long j) {
        return i * j;
    }
    public long exclude(long i, long j) {
        return i * j;
    }
}
CompilerOracle: inline com/kevin/jit/JITTest.inline
CompilerOracle: dontinline com/kevin/jit/JITTest.dontinline
CompilerOracle: exclude com/kevin/jit/JITTest.exclude
start: 1631608323025
exec inline: 52
exec dontinline: 232
### Excluding compile: com.kevin.jit.JITTest::exclude
exec exclude: 6958
  • 开启即时编译&方法内联时执行代码时间为52
  • 开启即时编译&不开启方法内联时执行代码时间为232
  • 不开启即时编译时执行代码时间为6958