• jvm虚拟机浅谈(二)


    一、方法调用

    指令名称

    描述

    invokestatic

    用于调用静态方法

    invokespecial

    用于调用私有实例方法、构造器,以及使用 super 关键字调用父类的实例方法或构造器,和所实现接口的default方法

    invokevirtual

    用于调用非私有实例方法

    invokeinterface

    用于调用接口方法

    invokedynamic

    用于调用动态方法

    1.1 虚方法调用和非虚方法调用

    虚方法调用:可以被子类重写的方法调用,需要运行时才能确定具体的调用类型。接口方法调用(invokeinterface 指令)和非私有实例方法调用(invokevirtual 指令)都属于虚方法调用。

    非虚方法调用:被invokestatic和invokespecial指令调用的方法,在解析阶段可以确定唯一的调用版本。静态方法、私有方法、实例构造器、父类方法4种,再加上被final修饰的方法(尽管它使用invokevirtual指令调用),这五种方法调用在类加载的时候就可以把符号引用解析为该方法的直接引用。

    1.2 静态绑定和动态绑定

    静态绑定:包括用于调用静态方法的 invokestatic 指令,和用于调用构造器、私有实例方法以及超类非私有实例方法的 invokespecial 指令。如果虚方法调用指向一个标记为 final 的方法,那么 Java 虚拟机也可以静态绑定该虚方法调用的目标方法。

    动态绑定:Java 虚拟机需要根据调用者的动态类型,来确定虚方法调用的目标方法。

    1.3 java重写和jvm重写的区别

    java的重写:指的是方法名相同并且参数类型也相同的方法之间的关系。

    Java虚拟机的重写:除了方法名和参数类型之外,返回类型也必须一致。

    对于 Java 语言中重写而 Java 虚拟机中非重写的情况,编译器会通过生成桥接方法来实现 Java 中的重写语义。

    eg:当一个声明类型为 Merchant,实际类型为 NaiveMerchant 的对象,调用 actionPrice 方法时,字节码里的符号引用指向的是 Merchant.actionPrice(double,Customer) 方法。Java 虚拟机将动态绑定至 NaiveMerchant 类的桥接方法之中,并且调用其 actionPrice(double,Customer) 方法。

    1. interface Customer {
    2. boolean isVIP();
    3. }
    4. class Merchant {
    5. public Number actionPrice(double price, Customer customer) {
    6. return Double.doubleToLongBits(2.0);
    7. }
    8. }
    9. public class NaiveMerchant extends Merchant {
    10. @Override
    11. public Double actionPrice(double price, Customer customer) {
    12. return 3.0;
    13. }
    14. public static void main(String[] args) {
    15. Merchant naiveMerchant = new NaiveMerchant();
    16. Number price =naiveMerchant.actionPrice(1.0d,null);
    17. }
    18. }

    javap -v 反编译如下

    1. public class NaiveMerchant extends Merchant
    2. minor version: 0
    3. major version: 55
    4. flags: (0x0021) ACC_PUBLIC, ACC_SUPER
    5. this_class: #5 // NaiveMerchant
    6. super_class: #9 // Merchant
    7. interfaces: 0, fields: 0, methods: 4, attributes: 1
    8. Constant pool:
    9. #1 = Methodref #9.#21 // Merchant."":()V
    10. #2 = Double 3.0d
    11. #4 = Methodref #22.#23 // java/lang/Double.valueOf:(D)Ljava/lang/Double;
    12. #5 = Class #24 // NaiveMerchant
    13. #6 = Methodref #5.#21 // NaiveMerchant."":()V
    14. #7 = Methodref #9.#25 // Merchant.actionPrice:(DLCustomer;)Ljava/lang/Number;
    15. #8 = Methodref #5.#26 // NaiveMerchant.actionPrice:(DLCustomer;)Ljava/lang/Double;
    16. #9 = Class #27 // Merchant
    17. #10 = Utf8
    18. #11 = Utf8 ()V
    19. #12 = Utf8 Code
    20. #13 = Utf8 LineNumberTable
    21. #14 = Utf8 actionPrice
    22. #15 = Utf8 (DLCustomer;)Ljava/lang/Double;
    23. #16 = Utf8 main
    24. #17 = Utf8 ([Ljava/lang/String;)V
    25. #18 = Utf8 (DLCustomer;)Ljava/lang/Number;
    26. #19 = Utf8 SourceFile
    27. #20 = Utf8 NaiveMerchant.java
    28. #21 = NameAndType #10:#11 // "":()V
    29. #22 = Class #28 // java/lang/Double
    30. #23 = NameAndType #29:#30 // valueOf:(D)Ljava/lang/Double;
    31. #24 = Utf8 NaiveMerchant
    32. #25 = NameAndType #14:#18 // actionPrice:(DLCustomer;)Ljava/lang/Number;
    33. #26 = NameAndType #14:#15 // actionPrice:(DLCustomer;)Ljava/lang/Double;
    34. #27 = Utf8 Merchant
    35. #28 = Utf8 java/lang/Double
    36. #29 = Utf8 valueOf
    37. #30 = Utf8 (D)Ljava/lang/Double;
    38. {
    39. public NaiveMerchant();
    40. descriptor: ()V
    41. flags: (0x0001) ACC_PUBLIC
    42. Code:
    43. stack=1, locals=1, args_size=1
    44. 0: aload_0
    45. 1: invokespecial #1 // Method Merchant."":()V
    46. 4: return
    47. LineNumberTable:
    48. line 12: 0
    49. public java.lang.Double actionPrice(double, Customer);
    50. descriptor: (DLCustomer;)Ljava/lang/Double;
    51. flags: (0x0001) ACC_PUBLIC
    52. Code:
    53. stack=2, locals=4, args_size=3
    54. 0: ldc2_w #2 // double 3.0d
    55. 3: invokestatic #4 // Method java/lang/Double.valueOf:(D)Ljava/lang/Double;
    56. 6: areturn
    57. LineNumberTable:
    58. line 15: 0
    59. public static void main(java.lang.String[]);
    60. descriptor: ([Ljava/lang/String;)V
    61. flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    62. Code:
    63. stack=4, locals=3, args_size=1
    64. 0: new #5 // class NaiveMerchant
    65. 3: dup
    66. 4: invokespecial #6 // Method "":()V
    67. 7: astore_1
    68. 8: aload_1
    69. 9: dconst_1
    70. 10: aconst_null
    71. 11: invokevirtual #7 // Method Merchant.actionPrice:(DLCustomer;)Ljava/lang/Number;
    72. 14: astore_2
    73. 15: return
    74. LineNumberTable:
    75. line 19: 0
    76. line 20: 8
    77. line 21: 15
    78. public java.lang.Number actionPrice(double, Customer);
    79. descriptor: (DLCustomer;)Ljava/lang/Number;
    80. flags: (0x1041) ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    81. Code:
    82. stack=4, locals=4, args_size=3
    83. 0: aload_0
    84. 1: dload_1
    85. 2: aload_3
    86. 3: invokevirtual #8 // Method actionPrice:(DLCustomer;)Ljava/lang/Double;
    87. 6: areturn
    88. LineNumberTable:
    89. line 12: 0
    90. }

    在编译过程中,我们并不知道目标方法的具体内存地址。因此,Java编译器会暂时用符号引用来表示该目标方法。这一符号引用包括目标方法所在的类或接口的名字,以及目标方法的方法名和方法描述符。符号引用存储在 class 文件的常量池之中。根据目标方法是否为接口方法,这些引用可分为接口符号引用和非接口符号引用。在执行使用了符号引用的字节码前,Java 虚拟机需要解析这些符号引用,并替换为实际引用。

    1.4 解析符号引用

    在C中查找符合名字及描述符的方法。
    如果没有找到,在C的父类中继续搜索,直至Object类。
    如果没有找到,在C所直接实现或间接实现的接口中搜索,这一步搜索得到的目标方法必须是非私有、非静态的

    对于非接口符号引用,假定该符号引用所指向的类为 C,则 Java 虚拟机会按照如下步骤进行查找。

    在I中查找符合名字及描述符的方法。
    如果没有找到,在Object类中的公有实例方法中搜索。
    如果没有找到,则在I的超接口中搜索,这一步搜索得到的目标方法必须是非私有、非静态的。

    经过上述的解析步骤之后,符号引用会被解析成实际引用。对于可以静态绑定的方法调用而言,实际引用是一个指向方法的指针。对于需要动态绑定的方法调用而言,实际引用则是一个方法表的索引。

    1.5 方法表

    在类加载的准备阶段,在类的方法区中构造与该类相关联的方法表。方法表分invokevirtual使用的虚方法表(virtual method table,vtable)和invokeinterface使用的接口方法表(interface method table,itable)。方法表本质上是一个数组,每个数组元素指向一个当前类及其祖先类中非私有的实例方法。这些方法可能是具体的、可执行的方法,也可能是没有相应字节码的抽象方法。

    方法表满足两个特质:

    其一,子类方法表中包含父类方法表中的所有方法。

    虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表中的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。如果子类中重写了这个方法,子类虚方法表中的地址也会被替换为指向子类实现版本的入口地址。

    eg:

    1. public class Dispatch {
    2. static class QQ {}
    3. static class _360 {}
    4. public static class Father {
    5. public void hardChoice(QQ arg) {
    6. System.out.println("father choose qq");
    7. }
    8. public void hardChoice(_360 arg) {
    9. System.out.println("father choose 360");
    10. }
    11. }
    12. public static class Son extends Father {
    13. public void hardChoice(QQ arg) {
    14. System.out.println("son choose qq");
    15. }
    16. public void hardChoice(_360 arg) {
    17. System.out.println("son choose 360");
    18. }
    19. }
    20. public static void main(String[] args) {
    21. Father father = new Father();
    22. Father son = new Son();
    23. father.hardChoice(new _360());
    24. son.hardChoice(new QQ());
    25. }
    26. }

    其二,子类方法在方法表中的索引值,与它所重写的父类方法的索引值相同。

    为了程序实现方便,具有相同签名的方法,在父类、子类的虚方法表中都应当具有一样的索引序号,这样当类型变换时,仅需要变更查找的虚方法表,就可以从不同的虚方法表中按索引转换出所需的入口地址。

    在解析虚方法调用时,Java 虚拟机会纪录下所声明的目标方法的索引值,并且在运行过程中根据这个索引值查找具体的目标方法。

    eg:

    1. abstract class Passenger {
    2. abstract void passThroughImmigration();
    3. @Override
    4. public String toString() {
    5. return null;
    6. }
    7. }
    8. class ForeignerPassenger extends Passenger {
    9. @Override
    10. void passThroughImmigration() {
    11. /* 外国人通道 */
    12. }
    13. }
    14. class ChinesePassenger extends Passenger {
    15. @Override
    16. void passThroughImmigration() {
    17. /* 中国人通道 */
    18. }
    19. void visitDutyFreeShops() {
    20. /* 逛免税店 */
    21. }
    22. }

    相应的虚方法表:

    1.6 内联缓存

    内联缓存是一种加快动态绑定的优化技术。它能够缓存虚方法调用中调用者的动态类型,以及该类型所对应的目标方法。在之后的执行过程中,如果碰到已缓存的类型,内联缓存便会直接调用该类型所对应的目标方法。如果没有碰到已缓存的类型,内联缓存则会退化至使用基于方法表的动态绑定。对于内联缓存来说,有对应的单态内联缓存、多态内联缓存和超多态内联缓存。

    单态内联缓存,顾名思义,便是只缓存了一种动态类型以及它所对应的目标方法。它的实现非常简单:比较所缓存的动态类型,如果命中,则直接调用对应的目标方法。

    多态内联缓存,则缓存了多个动态类型及其目标方法。它需要逐个将所缓存的动态类型与当前动态类型进行比较,如果命中,则调用对应的目标方法。

    为了节省内存空间,Java 虚拟机只采用单态内联缓存。对于内联缓存中的内容,我们有两种思路。

    一是替换单态内联缓存中的纪录

    在最坏情况下,我们用两种不同类型的调用者,轮流执行该方法调用,那么每次进行方法调用都将替换内联缓存。也就是说,只有写缓存的额外开销,而没有用缓存的性能提升。

    另外一种选择则是劣化为超多态状态

    这也是 Java 虚拟机的具体实现方式。处于这种状态下的内联缓存,实际上放弃了优化的机会。它将直接访问方法表,来动态绑定目标方法。与替换内联缓存纪录的做法相比,它牺牲了优化的机会,但是节省了写缓存的额外开销。

    eg:

    1. public abstract class Passenger {
    2. abstract void passThroughImmigration();
    3. public static void main(String[] args) {
    4. Passenger a = new ChinesePassenger();
    5. Passenger b = new ForeignerPassenger();
    6. long current = System.currentTimeMillis();
    7. for (int i = 1; i <= 2_000_000_000; i++) {
    8. if (i % 100_000_000 == 0) {
    9. long temp = System.currentTimeMillis();
    10. System.out.println(temp - current);
    11. current = temp;
    12. }
    13. Passenger c = (i < 1_000_000_000) ? a : b;
    14. // Passenger c = (i % 2)==0? a : b;
    15. c.passThroughImmigration();
    16. }
    17. }
    18. }
    19. class ChinesePassenger extends Passenger {
    20. @Override void passThroughImmigration() {}
    21. }
    22. class ForeignerPassenger extends Passenger {
    23. @Override void passThroughImmigration() {}
    24. }

    为了消除方法内联的影响,使用以下命令。

    java -XX:CompileCommand='dontinline,*.passThroughImmigration' Passenger

    1. java -XX:CompileCommand='dontinline,*.passThroughImmigration' Passenger
    2. CompileCommand: dontinline *.passThroughImmigration
    3. 258 //缓存动态类型
    4. 262
    5. 255
    6. 284
    7. 274
    8. 259
    9. 257
    10. 254
    11. 257
    12. 257
    13. 371 //缓存失效,劣化为超多态
    14. 366
    15. 370
    16. 367
    17. 368
    18. 376
    19. 419
    20. 433
    21. 385
    22. 426

    1.7 方法内联

    在编译过程中遇到方法调用时,将目标方法的方法体纳入编译范围之中,并取代原方法调用的优化手段。

    以 getter/setter 为例,如果没有方法内联,在调用 getter/setter 时,程序需要保存当前方法的执行位置,创建并压入用于 getter/setter 的栈帧、访问字段、弹出栈帧,最后再恢复当前方法的执行。而当内联了对 getter/setter 的方法调用后,上述操作仅剩字段访问。

    在 C2 中,方法内联是在解析字节码的过程中完成的。每当碰到方法调用字节码时,C2 将决定是否需要内联该方法调用。如果需要内联,则开始解析目标方法的字节码。

    静态方法调用

    eg:

    1. public static boolean flag = true;
    2. public static int value0 = 0;
    3. public static int value1 = 1;
    4. public static int foo(int value) {
    5. int result = bar(flag);
    6. if (result != 0) {
    7. return result;
    8. } else {
    9. return value;
    10. }
    11. }
    12. public static int bar(boolean flag) {
    13. return flag ? value0 : value1;
    14. }

    foo方法的IR图(代码中间表示)(内联前)

    在编译 foo 方法时,其对应的 IR 图中将出现对 bar 方法的调用,即上图中的 5 号 Invoke 节点。如果内联算法判定应当内联对 bar 方法的调用时,那么即时编译器将开始解析 bar 方法的字节码,并生成对应的 IR 图,如下图所示。

    接下来,即时编译器便可以进行方法内联,把 bar 方法所对应的 IR 图纳入到对 foo 方法的编译中。具体的操作便是将 foo 方法的 IR 图中 5 号 Invoke 节点替换为 bar 方法的 IR 图。

    将flag、value0、value1定义成final类型

    1. public final static boolean flag = true;
    2. public final static int value0 = 0;
    3. public final static int value1 = 1;
    4. public static int foo(int value) {
    5. int result = bar(flag);
    6. if (result != 0) {
    7. return result;
    8. } else {
    9. return value;
    10. }
    11. }
    12. public static int bar(boolean flag) {
    13. return flag ? value0 : value1;
    14. }

    foo的IR图(内联后)

    进一步优化(死代码消除)

    foo的IR图(优化后)

    方法内联性能影响

    强制进行方法内联,如下命令:

    java -XX:CompileCommand='inline,*.passThroughImmigration' Passenger

    1. CompileCommand: inline *.passThroughImmigration
    2. 86
    3. 152
    4. 152
    5. 159
    6. 165
    7. 179
    8. 145
    9. 146
    10. 150
    11. 144
    12. 331
    13. 246
    14. 235
    15. 235
    16. 232
    17. 234
    18. 235
    19. 235
    20. 233
    21. 234

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

  • 相关阅读:
    亚马逊 CTO Werner Vogels:2023 年及未来五大技术趋势预测
    vue大型电商项目尚品汇(后台篇)day03
    linux下安装/升级GCC到较高版本
    【Matplotlib绘制图像大全】(二十):Matplotlib给图片添加水印
    C语言概念知识扫盲(不定期补充更新)
    关于git创建分支以及主分支相互合并操作记录
    cubeIDE开发,基于已有的STM32CubeMX (.ioc)创建工程文件
    DDRx寻址原理
    java计算机毕业设计新能源汽车租赁管理系统源程序+mysql+系统+lw文档+远程调试
    Android教程之Android Compose 中实现类似链接反应弹出窗口的弹出窗口(教程含源码)
  • 原文地址:https://blog.csdn.net/wuweiwoshishei/article/details/126380526