• JVM虚拟机浅谈(三)


    一、即时编译

    1.1 基本概念

    HotSpot 虚拟机包含多个即时编译器 C1、C2 和 Graal(JDK 9后才有)。

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

    对于执行时间较长的,或者对峰值性能有要求的程序,我们采用生成代码执行效率较快的 C2,对应参数 -server。

    Java 7 引入了分层编译(对应参数 -XX:+TieredCompilation)的概念,综合了 C1 的启动性能优势和 C2 的峰值性能优势。

    Java 8 默认开启了分层编译。不管是开启还是关闭分层编译,原本用来选择即时编译器的参数 -client 和 -server 都是无效的。当关闭分层编译的情况下,Java 虚拟机将直接采用 C2。如果你希望只是用 C1,那么你可以在打开分层编译的情况下使用参数 -XX:TieredStopAtLevel=1。在这种情况下,Java 虚拟机会在解释执行之后直接由 1 层的 C1 进行编译。

    1.2 分层编译

    分层编译将 Java 虚拟机的执行状态分为了五个层次。

    ●level 0:interpreter解释执行

    ●level 1:C1编译,无profiling

    ●level 2:C1编译,仅方法调用次数及循环回边执行次数的profiling

    ●level 3:C1编译,除level 2中的profiling外还包括分支 profile(针对分支跳转字节码,包括跳转次数和不跳转次数)以及receiver type(针对成员方法调用或类检测,如checkcast,instanceof,aastore字节码)的类型profile

    ●level 4:C2编译

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

    ●第一条执行路径,指的是通常情况下,一个方法先被解释执行(level 0),然后被C1编译(level 3),再然后被得到profile数据的C2编译(level 4)

    ●第二条执行路径,指的是编译对象非常简单的情况下,如getter和setter,虚拟机认为通过C1编译或通过C2编译并无区别,就会在3层编译后,直接由C1编译且不插入profiling代码(level 1)。

    ●第三条执行路径,指的是C1繁忙时,JVM会在解释执行时收集profiling,而后直接由 4 层的 C2 编译。

    ●第四条执行路径,指的是C2繁忙时,先由2层的C1编译再由3层的C1编译,这样可以减少方法在3层的执行时间,最终再交给C2执行。

    1.3 即时编译的触发

    Java 虚拟机是根据方法的调用次数以及循环回边的执行次数来触发即时编译的。

    这里的循环回边是一个控制流图中的概念。在字节码中,我们可以简单理解为往回跳转的指令。

    1. public static void foo(Object obj) {
    2. int sum = 0;
    3. for (int i = 0; i < 200; i++) {
    4. sum += i;
    5. }
    6. }

    上面这段代码将被编译为下面的字节码。其中,偏移量为 18 的字节码将往回跳至偏移量为 4 的字节码中。在解释执行时,每当运行一次该指令,Java 虚拟机便会将该方法的循环回边计数器加 1。

    1. public static void foo(java.lang.Object);
    2. Code:
    3. 0: iconst_0
    4. 1: istore_1
    5. 2: iconst_0
    6. 3: istore_2
    7. 4: iload_2
    8. 5: sipush 200
    9. 8: if_icmpge 21
    10. 11: iload_1
    11. 12: iload_2
    12. 13: iadd
    13. 14: istore_1
    14. 15: iinc 2, 1
    15. 18: goto 4
    16. 21: return

    循环尾部偏移量为 15 的字节码到循环头部偏移量为 11 的字节码的控制流边就是真正意义上的循环回边。也就是说,C1 将在这个位置插入增加循环回边计数器的代码。Java 虚拟机并不会对这些计数器进行同步操作,因此收集而来的执行次数也并非精确值。

    ●在不启用分层编译的情况下,当方法的调用次数和循环回边的次数的和,超过由参数 -XX:CompileThreshold 指定的阈值时(使用 C1 时,该值为 1500;使用 C2 时,该值为 10000),便会触发即时编译。

    ●当启用分层编译时,Java 虚拟机将不再采用由参数 -XX:CompileThreshold 指定的阈值(该参数失效),而是使用另一套阈值系统。在这套系统中,阈值的大小是动态调整的。

    1. public class CompilationTest {
    2. private static Random random = new Random();
    3. public static void main(String[] args) throws Exception {
    4. for (int i = 0; i < 150000; i++) {
    5. foo(true, 2);
    6. }
    7. int count = 0;
    8. int i = 0;
    9. while (i++ < 150000) {
    10. count += plus();
    11. }
    12. }
    13. public static int plus() {
    14. int count = 0;
    15. for (int i = 0; i < 10; i++) {
    16. count += random.nextInt(10);
    17. }
    18. return count;
    19. }
    20. public static int foo(boolean b, int i) {
    21. int v;
    22. if (b) {
    23. v = i;
    24. } else {
    25. v = (int) Math.sin(i);
    26. }
    27. if (v == i) {
    28. return 0;
    29. } else {
    30. return (int) Math.cos(v);
    31. }
    32. }
    33. }

    java -XX:+UnlockDiagnosticVMOptions -XX:+TieredCompilation -XX:+LogCompilation -XX:+PrintCompilation -XX:LogFile=live.log CompilationTest

    1. 141 1 n 0 java.lang.System::arraycopy (native) (static)
    2. 151 2 3 java.lang.String::hashCode (55 bytes)
    3. 158 5 4 java.lang.String::charAt (29 bytes)
    4. 163 4 3 java.lang.String::indexOf (70 bytes)
    5. 170 6 3 java.lang.String::equals (81 bytes)
    6. 175 12 3 java.lang.Object::<init> (1 bytes)
    7. 177 8 3 java.lang.String::length (6 bytes)
    8. 177 13 3 java.lang.Math::min (11 bytes)
    9. 180 14 3 java.lang.CharacterData::of (120 bytes)
    10. 183 7 3 java.lang.AbstractStringBuilder::ensureCapacityInternal (27 bytes)
    11. 186 19 1 java.lang.Object::<init> (1 bytes)
    12. 186 12 3 java.lang.Object::<init> (1 bytes) made not entrant
    13. 186 20 2 com.xwiam.jvm.profile.CompilationTest::foo (30 bytes)
    14. 188 21 4 com.xwiam.jvm.profile.CompilationTest::foo (30 bytes)
    15. 188 17 3 java.lang.StringBuilder::append (8 bytes)
    16. 190 18 3 java.lang.AbstractStringBuilder::append (29 bytes)
    17. 190 9 3 java.lang.Character::toLowerCase (9 bytes)
    18. 191 22 % 3 com.xwiam.jvm.profile.CompilationTest::main @ 2 (43 bytes)
    19. 192 23 n 0 sun.misc.Unsafe::compareAndSwapLong (native)
    20. 197 20 2 com.xwiam.jvm.profile.CompilationTest::foo (30 bytes) made not entrant
    21. 200 28 2 com.xwiam.jvm.profile.CompilationTest::plus (29 bytes)
    22. 200 24 2 java.util.concurrent.atomic.AtomicLong::get (5 bytes)
    23. 200 25 2 java.util.concurrent.atomic.AtomicLong::compareAndSet (13 bytes)
    24. 202 29 1 java.util.concurrent.atomic.AtomicLong::get (5 bytes)
    25. 203 24 2 java.util.concurrent.atomic.AtomicLong::get (5 bytes) made not entrant
    26. 203 26 2 java.util.Random::nextInt (74 bytes)
    27. 207 30 1 java.util.concurrent.atomic.AtomicLong::compareAndSet (13 bytes)
    28. 207 25 2 java.util.concurrent.atomic.AtomicLong::compareAndSet (13 bytes) made not entrant
    29. 208 27 2 java.util.Random::next (47 bytes)
    30. 208 32 4 java.util.Random::nextInt (74 bytes)
    31. 208 33 % 4 com.xwiam.jvm.profile.CompilationTest::plus @ 4 (29 bytes)
    32. 210 31 % 3 com.xwiam.jvm.profile.CompilationTest::main @ 24 (43 bytes)
    33. 210 3 3 java.lang.System::getSecurityManager (4 bytes)
    34. 210 34 4 java.util.Random::next (47 bytes)
    35. 210 11 3 java.lang.String::<init> (82 bytes)
    36. 213 35 3 com.xwiam.jvm.profile.CompilationTest::main (43 bytes)
    37. 215 10 3 java.lang.CharacterDataLatin1::toLowerCase (39 bytes)
    38. 217 16 1 java.lang.ref.Reference::get (5 bytes)
    39. 217 15 1 sun.instrument.TransformerManager::getSnapshotTransformerList (5 bytes)
    40. 220 27 2 java.util.Random::next (47 bytes) made not entrant
    41. 220 36 % 4 com.xwiam.jvm.profile.CompilationTest::main @ 24 (43 bytes)
    42. 222 26 2 java.util.Random::nextInt (74 bytes) made not entrant
    43. 227 31 % 3 com.xwiam.jvm.profile.CompilationTest::main @ -2 (43 bytes) made not entrant
    44. 235 36 % 4 com.xwiam.jvm.profile.CompilationTest::main @ -2 (43 bytes) made not entrant

    ●第一列:时间(毫秒)

    ●第二列:JVM维护的编译ID

    ●第三列:一些标识,比如上面出现的n和s,n表示是否是native方法,显示在日志中为true,没显示为false。s表示是否是synchronized方法。此外还有:%表示是否是OSR编译,!表示是否包含异常处理器,b表示是否阻塞应用线程。

    ●第四列:编译的层次,0-4层

    ●第五列:编译的方法名。如果是 OSR 编译,那么方法名后面还会跟着 @以及循环所在的字节码。

    当发生去优化时,你将看到之前出现过的编译,不过被标记了“made not entrant"。它表示该方法不能再被进入。

    1. public static void main(java.lang.String[]) throws java.lang.Exception;
    2. Code:
    3. 0: iconst_0
    4. 1: istore_1
    5. 2: iload_1
    6. 3: ldc #2 // int 150000
    7. 5: if_icmpge 20
    8. 8: iconst_1
    9. 9: iconst_2
    10. 10: invokestatic #3 // Method foo:(ZI)I
    11. 13: pop
    12. 14: iinc 1, 1
    13. 17: goto 2
    14. 20: iconst_0
    15. 21: istore_1
    16. 22: iconst_0
    17. 23: istore_2
    18. 24: iload_2
    19. 25: iinc 2, 1
    20. 28: ldc #2 // int 150000
    21. 30: if_icmpge 42
    22. 33: iload_1
    23. 34: invokestatic #4 // Method plus:()I
    24. 37: iadd
    25. 38: istore_1
    26. 39: goto 24
    27. 42: return
    28. public static int plus();
    29. Code:
    30. 0: iconst_0
    31. 1: istore_0
    32. 2: iconst_0
    33. 3: istore_1
    34. 4: iload_1
    35. 5: bipush 10
    36. 7: if_icmpge 27
    37. 10: iload_0
    38. 11: getstatic #5 // Field random:Ljava/util/Random;
    39. 14: bipush 10
    40. 16: invokevirtual #6 // Method java/util/Random.nextInt:(I)I
    41. 19: iadd
    42. 20: istore_0
    43. 21: iinc 1, 1
    44. 24: goto 4
    45. 27: iload_0
    46. 28: ireturn
    47. public static int foo(boolean, int);
    48. Code:
    49. 0: iload_0
    50. 1: ifeq 9
    51. 4: iload_1
    52. 5: istore_2
    53. 6: goto 16
    54. 9: iload_1
    55. 10: i2d
    56. 11: invokestatic #7 // Method java/lang/Math.sin:(D)D
    57. 14: d2i
    58. 15: istore_2
    59. 16: iload_2
    60. 17: iload_1
    61. 18: if_icmpne 23
    62. 21: iconst_0
    63. 22: ireturn
    64. 23: iload_2
    65. 24: i2d
    66. 25: invokestatic #8 // Method java/lang/Math.cos:(D)D
    67. 28: d2i
    68. 29: ireturn
    69. static {};
    70. Code:
    71. 0: new #9 // class java/util/Random
    72. 3: dup
    73. 4: invokespecial #10 // Method java/util/Random."":()V
    74. 7: putstatic #5 // Field random:Ljava/util/Random;
    75. 10: return
    76. }

    1.4 OSR编译

    除了以方法为单位的即时编译之外,Java 虚拟机还存在着另一种以循环为单位的即时编译,叫做 On-Stack-Replacement(OSR)编译。循环回边计数器便是用来触发这种类型的编译的。

    OSR是一种在运行时替换正在运行的函数/方法的栈帧的技术。比如在一个函数/方法的执行过程中,在执行引擎的不同优化层级之间切换,可以是从低优化层级向高优化层级切换,也可以反过来。

    ●从低优化向高优化迁移:为了平衡启动性能(启动速度快,所以要初始开销小的执行模式)和顶峰性能(顶峰速度快,所以要更优化的执行模式,即便优化需要较大开销)

    ●从高优化向低优化迁移:这可以细分为许多情况,例如:

    *** 在高优化层级做了很激进的优化(例如假设某个类不会有别的子类、某个引用一定不是null、某个引用所指向的对象一定是某个具体类型,等),而这个激进的假设假如失效了的话,就必须退回到没有做这些优化的“安全”的低优化层级去继续执行。有了OSR(deoptimize)机制的支持,JIT编译器就可以对代码做非常激进的优化,性能受正确性要求的约束会得到放松,因而对常见代码模式可以生成更快的代码。

    *** 高优化层级不便于对代码做调试,如果某个方法之前已经被JIT优化编译了,而后来有调试器动态决定调试该方法,则让它从高优化层级退回到便于调试的低优化层级(例如解释器或者无优化的JIT编译版本的代码)去执行。

    二、 Profiling优化以及去优化

    2.1 基于分支 profile 的优化

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

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

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

    在现实中,分支 profile 出现仅跳转或者仅不跳转的情况并不多见。当然,即时编译器对分支 profile 的利用也不仅限于“剪枝”。它还会根据分支 profile,计算每一条程序执行路径的概率,以便某些编译器优化优先处理概率较高的路径。(Graal编译器)

    2.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 #4 // class java/lang/Exception
    13. 4: ifeq 12
    14. 7: aload_0
    15. 8: invokestatic #5 // Method java/lang/System.identityHashCode:(Ljava/lang/Object;)I
    16. 11: ireturn
    17. 12: aload_0
    18. 13: invokevirtual #6 // Method java/lang/Object.hashCode:()I
    19. 16: ireturn

    假设应用程序调用该方法时,所传入的 Object 皆为 Integer 实例。那么,偏移量为 1 的 instanceof 指令的类型 profile 仅包含 Integer,偏移量为 4 的分支跳转语句的分支 profile 中不跳转的次数为 0,偏移量为 13 的方法调用指令的类型 profile 仅包含 Integer。

    在 Java 虚拟机中,如果 instanceof 的目标类型是 final 类型,那么 Java 虚拟机仅需比较测试对象的动态类型是否为该 final 类型。果目标类型不是 final 类型,比如说我们例子中的 Exception,那么 Java 虚拟机需要从测试对象的动态类型开始,依次测试该类,该类的父类、祖先类,该类所直接实现或者间接实现的接口是否与目标类型一致。

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

    根据数据流分析,上述代码可以最终优化为极其简单的形式

    2.3 去优化

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

    去优化的过程相当复杂。由于即时编译器采用了许多优化方式,其生成的代码和原本的字节码的差异非常之大。

    去优化的过程中,需要将当前机器码的执行状态转换至某一字节码之前的执行状态,并从该字节码开始执行。这便要求即时编译器在编译过程中记录好这两种执行状态的映射。

    当根据映射关系创建好对应的解释执行栈桢后,Java 虚拟机便会采用 OSR 技术,动态替换栈上的内容,并在目标字节码处开始解释执行。

    此外,在调用 Java 虚拟机的去优化方法时,即时编译器生成的机器码可以根据产生去优化的原因来决定是否保留这一份机器码,以及何时重新编译对应的 Java 方法。

    ●如果去优化的原因与优化无关,即使重新编译也不会改变生成的机器码,那么生成的机器码可以在调用去优化方法时传入 Action_None,表示保留这一份机器码,在下一次调用该方法时重新进入这一份机器码。

    ●如果去优化的原因与静态分析的结果有关,例如类层次分析,那么生成的机器码可以在调用去优化方法时传入 Action_Recompile,表示不保留这一份机器码,但是可以不经过重新 profile,直接重新编译。

    ●如果去优化的原因与基于 profile 的激进优化有关,那么生成的机器码需要在调用去优化方法时传入 Action_Reinterpret,表示不保留这一份机器码,而且需要重新收集程序的 profile。

    三、 方法内联

    3.1 方法内联的条件

    方法内联能够触发更多的优化。通常而言,内联越多,生成代码的执行效率越高。然而,对于即时编译器来说,内联越多,编译时间也就越长,而程序达到峰值性能的时刻也将被推迟。

    此外,内联越多也将导致生成的机器码越长。在 Java 虚拟机里,编译生成的机器码会被部署到 Code Cache 之中。这个 Code Cache 是有大小限制的(由 Java 虚拟机参数 -XX:ReservedCodeCacheSize 控制)。

    这就意味着,生成的机器码越长,越容易填满 Code Cache,从而出现 Code Cache 已满,即时编译已被关闭的警告信息(CodeCache is full. Compiler has been disabled)。

    ●由 -XX:CompileCommand 中的 inline 指令指定的方法,以及由 @ForceInline 注解的方法(仅限于 JDK 内部方法),会被强制内联。而由 -XX:CompileCommand 中的 dontinline 指令或 exclude 指令(表示不编译)指定的方法,以及由 @DontInline 注解的方法(仅限于 JDK 内部方法),则始终不会被内联。

    ●如果调用字节码对应的符号引用未被解析、目标方法所在的类未被初始化,或者目标方法是 native 方法,都将导致方法调用无法内联。

    ●C2 不支持内联超过 9 层的调用(可以通过虚拟机参数 -XX:MaxInlineLevel 调整),以及 1 层的直接递归调用(可以通过虚拟机参数 -XX:MaxRecursiveInlineLevel 调整)。(如果方法 a 调用了方法 b,而方法 b 调用了方法 c,那么我们称 b 为 a 的 1 层调用,而 c 为 a 的 2 层调用。)

    ●最后,即时编译器将根据方法调用指令所在的程序路径的热度,目标方法的调用次数及大小,以及当前 IR 图的大小来决定方法调用能否被内联。

    总体来说,即时编译器中的内联算法更青睐于小方法。

    3.2 虚方法调用的方法内联

    对于需要动态绑定的虚方法调用来说,即时编译器需要先对虚方法调用进行去虚化(devirtualize),即转换为一个或多个直接调用,然后才能进行方法内联。

    3.2.2 完全去虚化

    完全去虚化是通过类型推导或者类层次分析(class hierarchy analysis),识别虚方法调用的唯一目标方法,从而将其转换为直接调用的一种优化手段。它的关键在于证明虚方法调用的目标方法是唯一的。

    ●基于类型推导的完全去虚化

    基于类型推导的完全去虚化将通过数据流分析推导出调用者的动态类型,从而确定具体的目标方法。

    1. public class TypeDeductionDevirtualization {
    2. static abstract class BinaryOp {
    3. public abstract int apply(int a, int b);
    4. }
    5. static class Add extends BinaryOp {
    6. public int apply(int a, int b) {
    7. return a + b;
    8. }
    9. }
    10. static class Sub extends BinaryOp {
    11. public int apply(int a, int b) {
    12. return a - b;
    13. }
    14. }
    15. public static int foo() {
    16. BinaryOp op = new Add();
    17. return op.apply(2, 1);
    18. }
    19. public static void main(String[] args) {
    20. for (int i = 0; i < 400_000; i++) {
    21. foo();
    22. }
    23. }
    24. }
    1. public static int foo();
    2. Code:
    3. 0: new #2 // class com/xwiam/jvm/devirtualization/TypeDeductionDevirtualization$Add
    4. 3: dup
    5. 4: invokespecial #3 // Method com/xwiam/jvm/devirtualization/TypeDeductionDevirtualization$Add."":()V
    6. 7: astore_0
    7. 8: aload_0
    8. 9: iconst_2
    9. 10: iconst_1
    10. 11: invokevirtual #4 // Method com/xwiam/jvm/devirtualization/TypeDeductionDevirtualization$BinaryOp.apply:(II)I
    11. 14: ireturn

    foo 方法的 IR 图(方法内联前)

    foo 方法的 IR 图(方法内联后)

    ●基于类层次分析的完全去虚化

    1. public class DeoptimizationJitTest {
    2. static abstract class BinaryOp {
    3. public abstract int apply(int a, int b);
    4. }
    5. static class Add extends BinaryOp {
    6. public int apply(int a, int b) {
    7. return a + b;
    8. }
    9. }
    10. static class Sub extends BinaryOp {
    11. public int apply(int a, int b) {
    12. return a - b;
    13. }
    14. }
    15. public static int test(BinaryOp op) {
    16. return op.apply(2, 1);
    17. }
    18. public static void main(String[] args) throws Exception {
    19. Add add = new Add();
    20. for (int i = 0; i < 400_000; i++) {
    21. test(add);
    22. }
    23. Thread.sleep(2000);
    24. System.out.println("Loading Sub");
    25. Sub[] array = new Sub[0]; // Load class Sub
    26. }
    27. }

    java -XX:+UnlockDiagnosticVMOptions -XX:+TieredCompilation -XX:+LogCompilation -XX:LogFile=live.log -XX:+PrintCompilation DeoptimizationJitTest

    1. 153 1 n 0 java.lang.System::arraycopy (native) (static)
    2. 172 2 n 0 java.lang.Thread::currentThread (native) (static)
    3. 180 4 3 java.lang.String::hashCode (55 bytes)
    4. 184 6 4 java.lang.String::charAt (29 bytes)
    5. 195 15 3 java.lang.String::indexOf (70 bytes)
    6. 200 7 3 java.lang.String::equals (81 bytes)
    7. 205 20 3 java.lang.String::startsWith (72 bytes)
    8. 206 3 3 java.lang.Object::<init> (1 bytes)
    9. 206 13 3 java.lang.String::length (6 bytes)
    10. 207 5 3 java.lang.Math::min (11 bytes)
    11. 208 22 3 sun.nio.cs.UTF_8$Encoder::encode (359 bytes)
    12. 215 23 2 com.xwiam.jvm.devirtualization.DeoptimizationJitTest::test (7 bytes)
    13. 216 24 2 com.xwiam.jvm.devirtualization.DeoptimizationJitTest$Add::apply (4 bytes)
    14. 216 14 3 java.lang.AbstractStringBuilder::ensureCapacityInternal (27 bytes)
    15. 216 25 4 com.xwiam.jvm.devirtualization.DeoptimizationJitTest::test (7 bytes)
    16. 218 26 1 com.xwiam.jvm.devirtualization.DeoptimizationJitTest$Add::apply (4 bytes)
    17. 219 24 2 com.xwiam.jvm.devirtualization.DeoptimizationJitTest$Add::apply (4 bytes) made not entrant
    18. 219 19 1 java.util.ArrayList::size (5 bytes)
    19. 219 16 3 java.lang.AbstractStringBuilder::append (50 bytes)
    20. 221 17 3 java.lang.String::getChars (62 bytes)
    21. 221 23 2 com.xwiam.jvm.devirtualization.DeoptimizationJitTest::test (7 bytes) made not entrant
    22. 222 27 % 3 com.xwiam.jvm.devirtualization.DeoptimizationJitTest::main @ 10 (47 bytes)
    23. 224 28 3 com.xwiam.jvm.devirtualization.DeoptimizationJitTest::main (47 bytes)
    24. 226 18 3 java.util.Arrays::copyOfRange (63 bytes)
    25. 226 29 % 4 com.xwiam.jvm.devirtualization.DeoptimizationJitTest::main @ 10 (47 bytes)
    26. 227 12 1 java.lang.ref.Reference::get (5 bytes)
    27. 227 8 3 java.lang.CharacterData::of (120 bytes)
    28. 228 9 3 java.lang.CharacterDataLatin1::getProperties (11 bytes)
    29. 229 10 3 java.lang.Character::toLowerCase (9 bytes)
    30. 229 11 3 java.lang.CharacterDataLatin1::toLowerCase (39 bytes)
    31. 229 21 1 java.lang.ThreadLocal::access$400 (5 bytes)
    32. 230 27 % 3 com.xwiam.jvm.devirtualization.DeoptimizationJitTest::main @ -2 (47 bytes) made not entrant
    33. 1134 30 3 java.lang.System::getSecurityManager (4 bytes)
    34. 1336 31 3 java.lang.ThreadLocal::get (38 bytes)
    35. 1342 32 3 java.lang.ThreadLocal$ThreadLocalMap::access$000 (6 bytes)
    36. 1342 33 3 java.lang.ThreadLocal$ThreadLocalMap::getEntry (42 bytes)
    37. 1346 34 3 java.lang.StringBuilder::append (8 bytes)
    38. 1348 35 3 java.lang.AbstractStringBuilder::append (29 bytes)
    39. 1348 38 3 java.io.DataInputStream::readUTF (501 bytes)
    40. 1352 43 1 java.lang.Object::<init> (1 bytes)
    41. 1352 3 3 java.lang.Object::<init> (1 bytes) made not entrant
    42. 1353 44 3 java.io.DataInputStream::readFully (63 bytes)
    43. 1353 37 2 java.io.BufferedInputStream::getBufIfOpen (21 bytes)
    44. 1354 40 s 3 java.io.BufferedInputStream::read (113 bytes)
    45. 1355 39 s 3 java.io.BufferedInputStream::read (49 bytes)
    46. 1356 45 3 java.io.DataInputStream::readShort (40 bytes)
    47. 1357 41 3 java.io.BufferedInputStream::read1 (108 bytes)
    48. 1359 36 3 java.lang.String::<init> (82 bytes)
    49. 1361 42 3 java.io.DataInputStream::readUnsignedShort (39 bytes)
    50. 1361 46 3 java.util.HashMap::hash (20 bytes)
    51. 1364 49 3 java.util.HashMap::putVal (300 bytes)
    52. 1365 51 3 java.io.UnixFileSystem::normalize (75 bytes)
    53. 1366 47 3 java.lang.String::indexOf (7 bytes)
    54. 1366 50 3 java.lang.StringBuilder::append (8 bytes)
    55. 1366 48 3 java.util.HashMap::put (13 bytes)
    56. 1369 52 ! 3 java.io.BufferedReader::readLine (304 bytes)
    57. 1371 56 4 java.lang.String::hashCode (55 bytes)
    58. 1373 53 3 java.lang.String::lastIndexOf (52 bytes)
    59. 1373 54 3 java.lang.Character::toLowerCase (6 bytes)
    60. 1373 55 1 java.io.File::getPath (5 bytes)
    61. 1373 59 3 java.lang.String::startsWith (7 bytes)
    62. 1373 57 1 java.net.URL::getProtocol (5 bytes)
    63. 1373 58 1 java.net.URL::getRef (5 bytes)
    64. 1374 60 3 java.util.ArrayList::get (11 bytes)
    65. 1375 61 3 java.util.ArrayList::rangeCheck (22 bytes)
    66. 1375 62 ! 3 sun.misc.URLClassPath$JarLoader::getResource (85 bytes)
    67. 1376 4 3 java.lang.String::hashCode (55 bytes) made not entrant
    68. 1376 63 3 java.util.HashMap$Node::<init> (26 bytes)
    69. 1376 66 4 sun.reflect.ClassFileAssembler::emitByte (11 bytes)
    70. 1376 67 4 sun.reflect.ByteVectorImpl::add (38 bytes)
    71. 1376 68 3 sun.reflect.UTF8::utf8Length (81 bytes)
    72. 1377 69 ! 3 sun.reflect.UTF8::encode (191 bytes)
    73. 1378 70 3 sun.reflect.ClassFileAssembler::emitConstantPoolUTF8 (50 bytes)
    74. 1379 71 3 sun.reflect.ClassFileAssembler::emitShort (24 bytes)
    75. 1379 72 3 sun.reflect.ClassFileAssembler::cpi (22 bytes)
    76. 1379 73 3 sun.reflect.ByteVectorImpl::getLength (7 bytes)
    77. 1379 74 3 sun.reflect.ByteVectorImpl::get (26 bytes)
    78. 1380 64 3 sun.util.locale.LocaleUtils::isUpper (18 bytes)
    79. 1380 79 3 java.util.LinkedHashMap::afterNodeInsertion (40 bytes)
    80. 1380 80 s 3 java.lang.StringBuffer::append (13 bytes)
    81. 1380 75 1 java.util.LinkedHashMap::removeEldestEntry (2 bytes)
    82. 1380 76 3 java.util.LinkedHashMap::newNode (23 bytes)
    83. 1381 77 3 java.util.LinkedHashMap$Entry::<init> (10 bytes)
    84. 1381 78 3 java.util.LinkedHashMap::linkNodeLast (33 bytes)
    85. 1381 65 1 sun.instrument.TransformerManager::getSnapshotTransformerList (5 bytes)
    86. 1381 81 s 3 java.util.Hashtable::get (69 bytes)
    87. 1382 82 s 3 java.util.Hashtable::put (104 bytes)
    88. 1382 83 3 java.lang.AbstractStringBuilder::<init> (12 bytes)
    89. 1383 84 s 4 java.lang.StringBuffer::append (13 bytes)
    90. 1383 85 3 java.lang.AbstractStringBuilder::newCapacity (39 bytes)
    91. 1383 86 3 java.lang.String::<init> (10 bytes)
    92. 1390 80 s 3 java.lang.StringBuffer::append (13 bytes) made not entrant
    93. Loading Sub
    94. 2230 87 3 java.nio.charset.CharsetEncoder::maxBytesPerChar (5 bytes)
    95. 2232 88 3 java.lang.String::indexOf (166 bytes)
    96. 2232 89 1 java.net.URL::getHost (5 bytes)
    97. 2233 25 4 com.xwiam.jvm.devirtualization.DeoptimizationJitTest::test (7 bytes) made not entrant
    98. 2233 29 % 4 com.xwiam.jvm.devirtualization.DeoptimizationJitTest::main @ -2 (47 bytes) made not entrant

    3.2.3 条件去虚化

    条件去虚化则是将虚方法调用转换为若干个类型测试以及直接调用的一种优化手段。它的关键在于找出需要进行比较的类型。

    具体的原理非常简单,是将调用者的动态类型,依次与 Java 虚拟机所收集的类型 Profile 中记录的类型相比较。如果匹配,则直接调用该记录类型所对应的目标方法。

    1. public static int test(BinaryOp op) {
    2. return op.apply(2, 1);
    3. }

    假设编译时类型 Profile 记录了调用者的两个类型 Sub 和 Add,那么即时编译器可以据此进行条件去虚化,依次比较调用者的动态类型是否为 Sub 或者 Add,并内联相应的方法。其伪代码如下所示:

    1. public static int test(BinaryOp op) {
    2. if (op.getClass() == Sub.class) {
    3. return 2 - 1; // inlined Sub.apply
    4. } else if (op.getClass() == Add.class) {
    5. return 2 + 1; // inlined Add.apply
    6. } else {
    7. ... // 当匹配不到类型Profile中的类型怎么办?
    8. }
    9. }

    如果遍历完类型 Profile 中的所有记录,仍旧匹配不到调用者的动态类型,那么即时编译器有两种选择。

    ●第一,如果类型 Profile 是完整的,也就是说,所有出现过的动态类型都被记录至类型 Profile 之中,那么即时编译器可以让程序进行去优化,重新收集类型 Profile。

    ●第二,如果类型 Profile 是不完整的,也就是说,某些出现过的动态类型并没有记录至类型 Profile 之中,那么重新收集并没有多大作用。此时,即时编译器可以让程序进行原本的虚调用,通过内联缓存进行调用,或者通过方法表进行动态绑定。

    3.3 通过JITwatch对方法内联进行验证

    JITWatch 是一款用于分析和可视化 HotSpot JIT Compiler 的工具。

    java -XX:+UnlockDiagnosticVMOptions -XX:+TieredCompilation -XX:+LogCompilation -XX:LogFile=live.log -XX:+PrintCompilation DeoptimizationJitTest

    1. public class DeoptimizationInlineJitTest {
    2. static abstract class BinaryOp {
    3. public abstract int apply(int a, int b);
    4. }
    5. static class Add extends BinaryOp {
    6. public int apply(int a, int b) {
    7. return a + b;
    8. }
    9. }
    10. static class Sub extends BinaryOp {
    11. public int apply(int a, int b) {
    12. return a - b;
    13. }
    14. }
    15. public static int test(BinaryOp op) {
    16. return op.apply(2, 1);
    17. }
    18. public static void main(String[] args) throws Exception {
    19. BinaryOp op;
    20. for (int i = 0; i < 400_000; i++) {
    21. if (i % 2 == 0) {
    22. op = new Add();
    23. } else {
    24. op = new Sub();
    25. }
    26. test(op);
    27. }
    28. Thread.sleep(2000);
    29. System.out.println("Loading Sub");
    30. Sub[] array = new Sub[0]; // Load class Sub
    31. }
    32. }

    java -XX:+UnlockDiagnosticVMOptions -XX:+TieredCompilation -XX:+LogCompilation -XX:LogFile=live.log -XX:+PrintCompilation DeoptimizationJitTest

    1. 167 1 n 0 java.lang.System::arraycopy (native) (static)
    2. 173 2 3 java.lang.String::hashCode (55 bytes)
    3. 178 13 4 sun.reflect.ClassFileAssembler::emitByte (11 bytes)
    4. 178 14 4 sun.reflect.ByteVectorImpl::add (38 bytes)
    5. 178 3 4 java.lang.String::charAt (29 bytes)
    6. 195 6 3 java.lang.String::indexOf (70 bytes)
    7. 200 12 3 sun.nio.cs.UTF_8$Encoder::encode (359 bytes)
    8. 209 26 3 java.lang.String::startsWith (72 bytes)
    9. 209 15 3 sun.reflect.ClassFileAssembler::emitConstantPoolUTF8 (50 bytes)
    10. 211 16 ! 3 sun.reflect.UTF8::encode (191 bytes)
    11. 212 17 3 sun.reflect.UTF8::utf8Length (81 bytes)
    12. 213 8 2 java.lang.Object::<init> (1 bytes)
    13. 213 27 2 com.xwiam.jvm.devirtualization.DeoptimizationInlineJitTest$BinaryOp::<init> (5 bytes)
    14. 214 33 1 java.lang.Object::<init> (1 bytes)
    15. 214 8 2 java.lang.Object::<init> (1 bytes) made not entrant
    16. 214 28 2 com.xwiam.jvm.devirtualization.DeoptimizationInlineJitTest::test (7 bytes)
    17. 215 29 2 com.xwiam.jvm.devirtualization.DeoptimizationInlineJitTest$Add::<init> (5 bytes)
    18. 215 34 4 com.xwiam.jvm.devirtualization.DeoptimizationInlineJitTest$BinaryOp::<init> (5 bytes)
    19. 216 32 2 com.xwiam.jvm.devirtualization.DeoptimizationInlineJitTest$Sub::apply (4 bytes)
    20. 217 5 3 java.lang.String::equals (81 bytes)
    21. 217 27 2 com.xwiam.jvm.devirtualization.DeoptimizationInlineJitTest$BinaryOp::<init> (5 bytes) made not entrant
    22. 217 35 4 com.xwiam.jvm.devirtualization.DeoptimizationInlineJitTest::test (7 bytes)
    23. 218 37 4 com.xwiam.jvm.devirtualization.DeoptimizationInlineJitTest$Add::<init> (5 bytes)
    24. 219 29 2 com.xwiam.jvm.devirtualization.DeoptimizationInlineJitTest$Add::<init> (5 bytes) made not entrant
    25. 235 28 2 com.xwiam.jvm.devirtualization.DeoptimizationInlineJitTest::test (7 bytes) made not entrant
    26. 236 36 1 com.xwiam.jvm.devirtualization.DeoptimizationInlineJitTest$Sub::apply (4 bytes)
    27. 255 32 2 com.xwiam.jvm.devirtualization.DeoptimizationInlineJitTest$Sub::apply (4 bytes) made not entrant
    28. 275 30 2 com.xwiam.jvm.devirtualization.DeoptimizationInlineJitTest$Add::apply (4 bytes)
    29. 275 38 % 3 com.xwiam.jvm.devirtualization.DeoptimizationInlineJitTest::main @ 2 (64 bytes)
    30. 318 31 2 com.xwiam.jvm.devirtualization.DeoptimizationInlineJitTest$Sub::<init> (5 bytes)
    31. 318 4 3 java.lang.Math::min (11 bytes)
    32. Loading Sub

    参考链接:深入拆解Java虚拟机_JVM_Java底层-极客时间

  • 相关阅读:
    java split 末尾空值被截断了
    Ubuntu 22.10 (Kinetic Kudu) 发布
    【数据结构】插入排序
    《创业者必学的搞流量营销课》负责百万到年入千万,500W+粉丝操盘经验
    Nginx:Tomcat部署及优化(一)
    笔记:在Entity Framework Core中使用乐观并发控制来处理数据更新的冲突
    C++ 移动语义
    如何让开发者直接在应用后台控制用户的运动状态?
    【从零开始学习 SystemVerilog】2.10、SystemVerilog 数据类型—— Associative Array(关联数组)
    区块链搭建联盟链及控制台安装
  • 原文地址:https://blog.csdn.net/wuweiwoshishei/article/details/126401216