Java的JIT

什么是JIT:

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

  1. 基于采样的热点探测:
    采用这种方法的虚拟机会周期性地检查各个线程的栈顶,如果发现某些方法经常出现在栈顶,那这段方法代码就是“热点代码”。这种探测方法的好处是实现简单高效,还可以很容易地获取方法调用关系,缺点是很难精确地确认一个方法的热度,容易因为受到线程阻塞或别的外界因素的影响而扰乱热点探测。
  2. 基于计数器的热点探测:
    采用这种方法的虚拟机会为每个方法,甚至是代码块建立计数器,统计方法的执行次数,如果执行次数超过一定的阀值,就认为它是“热点方法”。这种统计方法实现复杂一些,需要为每个方法建立并维护计数器,而且不能直接获取到方法的调用关系,但是它的统计结果相对更加精确严谨。
    HotSpot虚拟机中使用的是第二种:基于计数器的热点探测方法,因此它为每个方法准备了两个计数器:方法调用计数器和回边计数器。
    • 方法调用计数器
    方法调用计数器用来统计方法调用的次数,在默认设置下,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间内方法被调用的次数。
    • 回边计数器
    用于统计一个方法中循环体代码执行的次数(准确地说,应该是回边的次数,因为并非所有的循环都是回边),在字节码中遇到控制流向后跳转的指令就称为“回边”。
    JIT编译。触发了JIT编译后,在默认设置下,执行引擎并不会同步等待编译请求完成,而是继续进入解释器按照解释方式执行字节码,直到提交的请求被编译器编译完成为止(编译工作在后台线程中进行)。当编译工作完成后,下一次调用该方法或代码时,就会使用已编译的版本。
    方法调用计数器触发即时编译的流程(回边计数器触发即时编译的过程类似)
    JIT优化JVM的性能
    通常JIT的有以下几种手段来优化JVM的性能
  3. 针对特定CPU型号的编译优化,JVM会利用不同CPU支持的SIMD指令集来编译热点代码,提升性能。像intel支持的SSE2指令集在特定情况下可以提升近40倍的性能。
  4. 减少查表次数。比如调用Object.equals()方法,如果运行时发现一直是String对象的equals,编译后的代码可以直接调用String.equals方法,跳过查找该调用哪个方法的步骤。
  5. 逃逸分析。JAVA变量默认是分配在主存的堆上,但是如果方法中的变量未逃出使用的生命周期,不会被外部方法或者线程引用,可以考虑在栈上分配内存,减少GC压力。另外逃逸分析可以实现锁优化等提升性能方法。
  6. 寄存器分配,部分变量可以分配在寄存器中,相对于主存读取,更大的提升读取性能。
  7. 针对热点代码编译好的机器码进行缓存。代码缓存具有固定的大小,并且一旦它被填满,JVM 则不能再编译更多的代码。
  8. 方法内联,也是JIT实现的非常有用的优化能力,同时是开发者能够简单参与JIT性能调优的地方。

方法内联是什么。为什么它能够提升性能

要搞清楚为什么方法内联有用,首先要知道当一个函数被调用的时候发生了什么

  1. 首先会有个栈,存储目前所有活跃的方法,以及它们的本地变量和参数
  2. 当一个新的方法被调用了,一个新的栈帧会被加到栈顶,分配的本地变量和参数会存储在这个栈帧
  3. 跳到目标方法代码执行
  4. 方法返回的时候,本地方法和参数会被销毁,栈顶被移除
  5. 返回原来地址执行
    因此,函数调用需要有一定的时间开销和空间开销,当一个方法体不大,但又频繁被调用时,这个时间和空间开销会相对变得很大,变得非常不划算,同时降低了程序的性能。
    方法内联就是把被调用方函数代码"复制"到调用方函数中,来减少因函数调用开销的技术。
    被内联前的代码
    private int add4(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 add4(int x1, int x2, int x3, int x4) {
    return x1 + x2 + x3 + x4;
    }
    JVM会自动的识别热点方法,并对它们使用方法内联优化。那么一段代码需要执行多少次才会触发JIT优化呢?通常这个值由-XX:CompileThreshold参数进行设置:
    1、使用client编译器时,默认为1500;
    2、使用server编译器时,默认为10000;
    但是一个方法就算被JVM标注成为热点方法,JVM仍然不一定会对它做方法内联优化。其中有个比较常见的原因就是这个方法体太大了,分为两种情况。
    • 如果方法是经常执行的,默认情况下,方法大小小于325字节的都会进行内联(可以通过** -XX:MaxFreqInlineSize=N来设置这个大小)
    • 如果方法不是经常执行的,默认情况下,方法大小小于35字节才会进行内联(可以通过
    -XX:MaxInlineSize=N **来设置这个大小)
    我们可以通过增加这个大小,以便更多的方法可以进行内联;但是除非能够显著提升性能,否则不推荐修改这个参数。因为更大的方法体会导致代码内存占用更多,更少的热点方法会被缓存,最终的效果不一定好。
    如果想要知道方法被内联的情况,可以使用下面的JVM参数来配置
    -XX:+PrintCompilation: Prints out when JIT compilation happens
    -XX:+UnlockDiagnosticVMOptions: Is needed to use flags like -XX:+PrintInlining
    -XX:+PrintInlining: Prints what methods were inlined

结论

热点方法的内联优化建议

  1. 更小的方法体
  2. 尽量使用final、private、static修饰符
  3. 使用+PrintInlining参数校验效果
posted @ 2020-10-17 15:45  技术-刘腾飞  阅读(889)  评论(0编辑  收藏  举报