Java 的 JIT 知识整理

JIT 介绍

JIT 编译器(just in time 即时编译器),当虚拟机发现某个方法或代码块运行特别频繁时,就会把这些代码认定为 热点代码(Hot Spot Code),为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各层次的优化,完成这项任务的正是 JIT 编译器

JIT 的工作原理

1598525622168

JIT 编译

对于 Java 代码,刚开始都是被编译器编译成字节码文件然后字节码文件会被交由 JVM 解释执行,所以可以说 Java 本身是一种半编译半解释执行的语言

当 JIT 编译启用时(默认是启用的),JVM 读入.class文件解释后,将其发给JIT编译器。JIT 编译器将字节码编译成本机机器代码

通常 Javac将程序源码编译转换成java字节码JVM通过解释字节码将其翻译成相应的机器指令,逐条读入,逐条解释翻译。
经过解释运行,其运行速度必定会比可运行的二进制字节码程序慢。为了提高运行速度,引入了 JIT 技术。

在执行时 JIT 会把翻译过的机器码保存起来,已备下次使用,因此从理论上来说,采用该 JIT 技术能够,能够接近曾经纯编译技术。

运行过程中会被即时编译器编译的热点代码有两类:被多次调用的方法被多次调用的循环体
这两种情况,编译器都是以整个方法作为编译对象,这种编译也是虚拟机中标准的编译方式。要知道一段代码或方法是不是热点代码,是不是需要触发即时编译,需要进行 Hot Spot Detection(热点探测)

热点判定方式

目前主要的热点 判定方式有以下两种:

  1. 基于采样的热点探测
    采用这种方法的虚拟机会周期性地检查各个线程的栈顶,如果发现某些方法经常出现在栈顶,那这段方法代码就是 “热点代码”。这种探测方法的好处是实现简单高效,还可以很容易地获取方法调用关系,缺点是很难精确地确认一个方法的热度,容易因为受到线程阻塞或别的外界因素的影响而扰乱热点探测。
  2. 基于计数器的热点探测
    采用这种方法的虚拟机会为每个方法,甚至是代码块建立计数器,统计方法的执行次数,如果执行次数超过一定的阀值,就认为它是 “热点方法”。这种统计方法实现复杂一些,需要为每个方法建立并维护计数器,而且不能直接获取到方法的调用关系,但是它的统计结果相对更加精确严谨。

HotSpot 虚拟机中使用的是第二种: 基于计数器的热点探测方法,因此它为每个方法准备了两个计数器:方法调用计数器和回边计数器

  • 方法调用计数器

    方法调用计数器用来统计方法调用的次数,在默认设置下,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间内方法被调用的次数。

  • 回边计数器

    用于统计一个方法中循环体代码执行的次数(准确地说,应该是回边的次数,因为并非所有的循环都是回边),在字节码中遇到控制流向后跳转的指令就称为 “回边”。

方法调用计数器触发即时编译的流程:

1598525962562

设置热点阈值

一段代码需要执行多少次才会触发 JIT 优化呢?通常这个值由 -XX:CompileThreshold 参数进行设置:

  1. 使用 client 编译器时,默认为 1500。

  2. 使用 server 编译器时,默认为 10000。

JIT 优化

  1. 开启服务端模式

    开启服务端模式以后就有即时编译器和解释器两种执行引擎,执行效率最高的是即时编译器,所以我们做 JIT 优化的目的是尽量使代码使用即时编译器

    参数设置:-server

  2. 增加内联函数的可能性

    增加函数内联的可能性能减少栈帧的创建,节约内存空间

    参数设置:

    使用 final 修饰函数向编译器建议可以内联,启动参数不宜设置,注意只是建议,具体是否内联看 JVM 决定

  3. 提高使用即时编译器的可能性

    小方法:写方法时尽量不要写得太大,让 JVM 尽可能使用即时编译器编译代码

    在启动项配置参数 - XX:CompileThreshold=10000,使得一个方法被调用超过 10000 次以后使用即时编译器编译为机器码

    OSR 编译阈值

    • 调用计数器,即方法被调用的次数,CompileThreshold,该值是指当方法被调用多少次后,就编译为机器码,client 模式默认为 1500 次,server 模式默认为 1 万次,可以在启动时添加 - XX:CompileThreshold=10000 来设置该值。
    • 回边计数器,即方法中循环执行部分代码的执行次数,OnStackReplacePercentage,该值用于 / 参与计算是否触发 OSR 编译的阈值,client 默认为 933,sever 默认为 140,可以通过 - XX:OnStackReplacePercentage=140 来设置。

    client 模式下的计算规则为:

    CompileThreshold*OnStackReplacePercentage/100

    server 模式下计算规则为:

    CompileThreshold *(OnStackReplacePercentage-InterpreterProfilePercentage)/100

    InterpreterProfilePercentage,默认为 33。

  4. 降低线程优先级

    Linux 不能设置,需要 root 权限

  5. 热度衰减与半衰周期