• JVM-即时编译


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

      HotSpot 虚拟机包含多个即时编译器 C1、C2 和 Graal(实验性质)。其中,Graal 是一个实验性质的即时编译器,可以通过参数 -XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler 启用,并且替换 C2。

    1. 分层编译模式  

      在 Java 7 以前,我们需要根据程序的特性选择对应的即时编译器。对于执行时间较短的,或者对启动性能有要求的程序,我们采用编译效率较快的 C1,对应参数 -client。对于执行时间较长的,或者对峰值性能有要求的程序,我们采用生成代码执行效率较快的 C2,对应参数 -server。

      Java 7 引入了分层编译(对应参数 -XX:+TieredCompilation)的概念,综合了 C1 的启动性能优势 C2 的峰值性能优势。分层编译将 Java 虚拟机的执行状态分为了五个层次。为了方便阐述,我用“C1 代码”来指代由 C1 生成的机器码,“C2 代码”来指代由 C2 生成的机器码。五个层级分别是:

        0. 解释执行;

    1. 执行不带 profiling 的 C1 代码;
    2. 执行仅带方法调用次数以及循环回边执行次数 profiling 的 C1 代码;
    3. 执行带所有 profiling 的 C1 代码;(除level 2中的profiling外还包括branch(针对分支跳转字节码)及receiver type(针对成员方法调用或类检测,如checkcast,instnaceof,aastore字节码)的profiling)
    4. 执行 C2 代码。

      通常情况下,C2 代码的执行效率要比 C1 代码的高出 30% 以上。然而,对于 C1 代码的三种状态,按执行效率从高至低则是 1 层 > 2 层 > 3 层。其中 1 层的性能比 2 层的稍微高一些,而 2 层的性能又比 3 层高出 30%。这是因为 profiling 越多,其额外的性能开销越大。

      这里解释一下,profiling指在程序执行过程中,收集能够反映程序执行状态的数据。这里所收集的数据我们称之为程序的 profile。 

    上图列举了4种编译模式(非全部)。

    • 通常情况下,一个方法先被解释执行(level 0),然后被C1编译(level 3),再然后被得到profile数据的C2编译(level 4)。
    • 如果编译对象非常简单(trivial method--非常简单的,如方法的字节码数目比较少(如 getter/setter),而且 3 层的 profiling 没有可收集的数据),虚拟机认为通过C1编译或通过C2编译并无区别,便会直接由C1编译且不插入profiling代码(level 1)。
    • 在C1忙碌的情况下,interpreter会触发profiling,而后方法会直接被C2编译;
    • 在C2忙碌的情况下,方法则会先由C1编译并保持较少的profiling(level 2),以获取较高的执行效率(与3级相比高30%)。 

    2. 即时编译的触发

      Java 虚拟机是根据方法的调用次数以及循环回边的执行次数(循环体内循环代码的执行次数(即for中代码的循环的次数))触发即时编译的。前面提到,Java 虚拟机在 0 层、2 层和 3 层执行状态时进行 profiling,其中就包含方法的调用次数和循环回边的执行次数。

      这里的循环回边是一个控制流图中的概念。在字节码中,我们可以简单理解为往回跳转的指令。(注意,这并不一定符合循环回边的定义。)

      实际上,Java 虚拟机并不会对这些计数器进行同步操作,因此收集而来的执行次数也并非精确值。不管如何,即时编译的触发并不需要非常精确的数值。只要该数值足够大,就能说明对应的方法包含热点代码。

    3. Profiling优化

    3.1 基于分支profile的优化

    例:

     1 public static int foo(boolean f, int in) {
     2   int v;
     3   if (f) {
     4     v = in;
     5   } else {
     6     v = (int) Math.sin(in);
     7   }
     8 
     9   if (v == in) {
    10     return 0;
    11   } else {
    12     return (int) Math.cos(v);
    13   }
    14 }
    15 // 编译而成的字节码:
    16 public static int foo(boolean, int);
    17   Code:
    18      0: iload_0
    19      1: ifeq          9
    20      4: iload_1
    21      5: istore_2
    22      6: goto          16
    23      9: iload_1
    24     10: i2d
    25     11: invokestatic  java/lang/Math.sin:(D)D
    26     14: d2i
    27     15: istore_2
    28     16: iload_2
    29     17: iload_1
    30     18: if_icmpne     23
    31     21: iconst_0
    32     22: ireturn
    33     23: iload_2
    34     24: i2d
    35     25: invokestatic java/lang/Math.cos:(D)D
    36     28: d2i
    37     29: ireturn

      假设应用程序调用该方法时,所传入的 boolean 值皆为 true。那么,偏移量为 1 以及偏移量为 18 的条件跳转指令所对应的分支 profile 中,跳转的次数都为 0。

      C2 可以根据这两个分支 profile 作出假设,在接下来的执行过程中,这两个条件跳转指令仍旧不会发生跳转。基于这个假设,C2 便不再编译这两个条件跳转语句所对应的 false 分支了。

      我们暂且不管当假设错误的时候会发生什么,先来看一看剩下来的代码。经过“剪枝”之后,在第二个条件跳转处,v 的值只有可能为所输入的 int 值。因此,该条件跳转可以进一步被优化掉。最终的结果是,在第一个条件跳转之后,C2 代码将直接返回 0。

    3.2 基于类型profile的优化

    例:

     1 public static int hash(Object in) {
     2   if (in instanceof Exception) {
     3     return System.identityHashCode(in);
     4   } else {
     5     return in.hashCode();
     6   }
     7 }
     8 // 编译而成的字节码:
     9 public static int hash(java.lang.Object);
    10   Code:
    11      0: aload_0
    12      1: instanceof java/lang/Exception
    13      4: ifeq          12
    14      7: aload_0
    15      8: invokestatic java/lang/System.identityHashCode:(Ljava/lang/Object;)I
    16     11: ireturn
    17     12: aload_0
    18     13: invokevirtual java/lang/Object.hashCode:()I
    19     16: ireturn

      在我们的例子中,instanceof 指令的类型 profile 仅包含 Integer。根据这个信息,即时编译器可以假设,在接下来的执行过程中,所输入的 Object 对象仍为 Integer 实例。

      因此,生成的代码将测试所输入的对象的动态类型是否为 Integer。如果是的话,则继续执行接下来的代码。(该优化源自 Graal,采用 C2 可能无法复现。)

      然后,即时编译器会采用和第一个例子中一致的针对分支 profile 的优化,以及对方法调用的条件去虚化内联。

      我会在接下来的篇章中详细介绍内联,这里先说结果:生成的代码将测试所输入的对象动态类型是否为 Integer。如果是的话,则执行 Integer.hashCode() 方法的实质内容,也就是返回该 Integer 实例的 value 字段。

     

    3.3 去优化

      和基于分支 profile 的优化一样,基于类型 profile 的优化同样也是作出假设,从而精简控制流以及数据流。这两者的核心都是假设

      对于分支 profile,即时编译器假设的是仅执行某一分支;对于类型 profile,即时编译器假设的是对象的动态类型仅为类型 profile 中的那几个。

      那么,当假设失败的情况下,程序将何去何从?

      Java 虚拟机给出的解决方案便是去优化,即从执行即时编译生成的机器码切换回解释执行。

      在生成的机器码中,即时编译器将在假设失败的位置上插入一个陷阱(trap)。该陷阱实际上是一条 call 指令,调用至 Java 虚拟机里专门负责去优化的方法。与普通的 call 指令不一样的是,去优化方法将更改栈上的返回地址,并不再返回即时编译器生成的机器码中。

      在上面的程序控制流图中,我画了很多红色方框的问号。这些问号便代表着一个个的陷阱。一旦踏入这些陷阱,便将发生去优化并切换至解释执行

  • 相关阅读:
    Mybatis学习笔记
    Java——设计模式
    Java——多线程
    Java——集合
    DAO层、Service层、Controller层、View层
    Mybatis整合spring
    Spring中的DI和IOC
    事务
    Xml实现AOP
    2018.3.10考试的试题解析
  • 原文地址:https://www.cnblogs.com/wxdlut/p/14201672.html
Copyright © 2020-2023  润新知