zoukankan      html  css  js  c++  java
  • JVM-JVM是如何执行方法调用的

    重载、重写

    void invoke(Object obj, Object... args) { ... }
    void invoke(String s, Object obj, Object... args) { ... }
    
    invoke(null, 1);    // 调用第二个invoke方法
    invoke(null, 1, 2); // 调用第二个invoke方法
    invoke(null, new Object[]{1}); // 只有手动绕开可变长参数的语法糖,
                                   // 才能调用第一个invoke方法

    重载的方法在编译过程中即可完成识别。具体到每一个方法调用,Java 编译器会根据所传入参数的声明类型(注意与实际类型区分)来选取重载方法。选取的过程共分为三个阶段:在不考虑对基本类型自动装拆箱(auto-boxing,auto-unboxing),以及可变长参数的情况下选取重载方法;如果在第 1 个阶段中没有找到适配的方法,那么在允许自动装拆箱,但不允许可变长参数的情况下选取重载方法;如果在第 2 个阶段中没有找到适配的方法,那么在允许自动装拆箱以及可变长参数的情况下选取重载方法。如果 Java 编译器在同一个阶段中找到了多个适配的方法,那么它会在其中选择一个最为贴切的,而决定贴切程度的一个关键就是形式参数类型的继承关系。

    在开头的例子中,当传入 null 时,它既可以匹配第一个方法中声明为 Object 的形式参数,也可以匹配第二个方法中声明为 String 的形式参数。由于 String 是 Object 的子类,因此 Java 编译器会认为第二个方法更为贴切。

    如果子类定义了与父类中非私有方法同名的方法,而且这两个方法的参数类型相同,那么这两个方法之间又是什么关系呢?如果这两个方法都是静态的,那么子类中的方法隐藏了父类中的方法。如果这两个方法都不是静态的,且都不是私有的,那么子类的方法重写了父类中的方法。

      在 Java 中,方法存在重载以及重写的概念,重载指的是方法名相同而参数类型不相同的方法之间的关系,重写指的是方法名相同并且参数类型也相同的方法之间的关系。

      Java 虚拟机识别方法的方式略有不同,除了方法名参数类型之外,它还会考虑返回类型。在 Java 虚拟机中,静态绑定指的是在解析时便能够直接识别目标方法的情况,而动态绑定则指的是需要在运行过程中根据调用者的动态类型来识别目标方法的情况。由于 Java 编译器已经区分了重载的方法,因此可以认为 Java 虚拟机中不存在重载。

    例:

     1 package javap.method;
     2 
     3 public class Animal {
     4 
     5     protected static void method1(String str) {
     6         System.out.println("Animal_static_method1" + str);
     7     }
     8 
     9     protected void method2(String str) {
    10         System.out.println("Animal_not_static_method2" + str);
    11     }
    12 
    13     // 1、Java语言:只要方法名、参数类型一致(不考虑返回值),就认为重复定义
    14     // 如下,java编译器会报:'method2(String)' is already defined in 'javap.method.Animal'
    15     //int method2(String str) {
    16     //}
    17 
    18     // 2、Java 虚拟机:识别方法的关键在于类名、方法名,方法描述符(method descriptor--参数类型、返回类型),
    19     // 所以如果手动添加上述方法到字节码,JVM是可以准确识别和区分的
    20 
    21     protected int method3() {
    22         System.out.println("Animal_not_static_method3");
    23         return 99;
    24     }
    25 
    26 }
    27 
    28 class Fish extends Animal {
    29 
    30     // 静态方法不可以写@Override,报错:Method does not override method from its superclass
    31     // @Override
    32     protected static void method1(String str) {
    33         System.out.println("Fish_static_method1" + str);
    34     }
    35 
    36     @Override
    37     protected void method2(String str) {
    38         System.out.println("Fish_not_static_method2" + str);
    39     }
    40 
    41     // 如果改为 void 返回值:报错  attempting to use incompatible return type
    42     protected int method3() {
    43         System.out.println("Fish_not_static_method3");
    44         return 100;
    45     }
    46 
    47 
    48 
    49     public static void main(String[] args) {
    50         Animal animal = new Fish();
    51         animal.method2("MainMethod2");  // 发生动态绑定
    52         System.out.println("************");
    53         // 此方法则是直接调用方法区中静态方法,无需经过方法表
    54         animal.method1("MainMethod1");  // 静态绑定,因为声明的时候animal是Animal类型
    55 
    56         System.out.println("########");
    57         Fish fish = new Fish();
    58         fish.method2("IsFish");
    59     }
    60 }

    javap结果:

    163   public static void main(java.lang.String[]);
    164     descriptor: ([Ljava/lang/String;)V
    165     flags: ACC_PUBLIC, ACC_STATIC
    166     Code:
    167       stack=2, locals=3, args_size=1
    168          0: new           #11                 // class javap/method/Fish
    169          3: dup
    170          4: invokespecial #12                 // Method "<init>":()V
    171          7: astore_1
    172          8: aload_1
    173          9: ldc           #13                 // String MainMethod2
    174         11: invokevirtual #14                 // Method javap/method/Animal.method2:(Ljava/lang/String;)V  -- 动态绑定
    175         14: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
    176         17: ldc           #15                 // String ************
    177         19: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    178         22: aload_1
    179         23: pop
    180         24: ldc           #16                 // String MainMethod1
    181         26: invokestatic  #17                 // Method javap/method/Animal.method1:(Ljava/lang/String;)V  -- 静态绑定,因为声明的时候是Animal类型
    182         29: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
    183         32: ldc           #18                 // String ########
    184         34: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    185         37: new           #11                 // class javap/method/Fish
    186         40: dup
    187         41: invokespecial #12                 // Method "<init>":()V
    188         44: astore_2
    189         45: aload_2
    190         46: ldc           #19                 // String IsFish
    191         48: invokevirtual #20                 // Method method2:(Ljava/lang/String;)V
    192         51: return
    193       LineNumberTable:
    194         line 49: 0
    195         line 50: 8
    196         line 51: 14
    197         line 52: 22
    198         line 54: 29
    199         line 55: 37
    200         line 56: 45
    201         line 57: 51
    202       LocalVariableTable:
    203         Start  Length  Slot  Name   Signature
    204             0      52     0  args   [Ljava/lang/String;
    205             8      44     1 animal   Ljavap/method/Animal;
    206            45       7     2  fish   Ljavap/method/Fish;
    207 }
    208 SourceFile: "Animal.java"

    桥接 

    在文中我曾提到,Java 的重写与 Java 虚拟机中的重写并不一致,但是编译器会通过生成桥接方法来弥补。

    例1:重写方法的返回类型不一致:

     1 package javap.method;
     2 
     3 public interface Customer {
     4     boolean isVIP();
     5 }
     6 class Merchant {
     7     public Number actionPrice(double price, Customer customer) {
     8         return price * 0.8;
     9     }
    10 }
    11 
    12 class NaiveMerchant extends Merchant {
    13     @Override
    14     public Double actionPrice(double price, Customer customer) {
    15         if (customer.isVIP()) {
    16             return price * 0.6;
    17         } else {
    18             return (Double) super.actionPrice(price, customer);
    19         }
    20     }
    21 }
     1 $ java -jar ../asmtools.jar jdis  NaiveMerchant.class
     2 package  javap/method;
     3 
     4 super class NaiveMerchant
     5         extends Merchant
     6         version 52:0
     7 {
     8 
     9 
    10 Method "<init>":"()V"
    11         stack 1 locals 1
    12 {
    13                 aload_0;
    14                 invokespecial   Method Merchant."<init>":"()V";
    15                 return;
    16         
    17 }
    18 
    19 public Method actionPrice:"(DLjavap/method/Customer;)Ljava/lang/Double;"
    20         stack 4 locals 4
    21 {
    22                 aload_3;
    23                 invokeinterface InterfaceMethod Customer.isVIP:"()Z",  1;
    24                 ifeq    L18;
    25                 dload_1;
    26                 ldc2_w  double 0.6d;
    27                 dmul;
    28                 invokestatic    Method java/lang/Double.valueOf:"(D)Ljava/lang/Double;";
    29                 areturn;
    30         L18:    stack_frame_type same;
    31                 aload_0;
    32                 dload_1;
    33                 aload_3;
    34                 invokespecial   Method Merchant.actionPrice:"(DLjavap/method/Customer;)Ljava/lang/Number;";
    35                 checkcast       class java/lang/Double;
    36                 areturn;
    37         
    38 }
    39 
    40 public bridge synthetic Method actionPrice:"(DLjavap/method/Customer;)Ljava/lang/Number;"
    41         stack 4 locals 4
    42 {
    43                 aload_0;
    44                 dload_1;
    45                 aload_3;
    46                 invokevirtual   Method actionPrice:"(DLjavap/method/Customer;)Ljava/lang/Double;";
    47                 areturn;
    48         
    49 }
    50 
    51 } // end Class NaiveMerchant

    例2:范型参数类型造成的方法参数类型不一致:

     1 package javap.method;
     2 
     3 public interface CustomerNew {
     4     boolean isVIP();
     5 }
     6 
     7 class VIP implements CustomerNew {
     8 
     9     public boolean isVIP() {
    10         return true;
    11     }
    12 }
    13 
    14 class MerchantNew<T extends CustomerNew> {
    15     public double actionPrice(double price, T customer) {
    16         return price * 0.8;
    17     }
    18 }
    19 
    20 class VIPOnlyMerchant extends MerchantNew<VIP> {
    21     @Override
    22     public double actionPrice(double price, VIP customer) {
    23         return price * 0.9; // 杀熟
    24     }
    25 }
     1 $ java -jar ../asmtools.jar jdis  VIPOnlyMerchant.class
     2 package  javap/method;
     3 
     4 super class VIPOnlyMerchant
     5         extends MerchantNew
     6         version 52:0
     7 {
     8 
     9 
    10 Method "<init>":"()V"
    11         stack 1 locals 1
    12 {
    13                 aload_0;
    14                 invokespecial   Method MerchantNew."<init>":"()V";
    15                 return;
    16         
    17 }
    18 
    19 public Method actionPrice:"(DLjavap/method/VIP;)D"
    20         stack 4 locals 4
    21 {
    22                 dload_1;
    23                 ldc2_w  double 0.9d;
    24                 dmul;
    25                 dreturn;
    26         
    27 }
    28 
    29 public bridge synthetic Method actionPrice:"(DLjavap/method/CustomerNew;)D"
    30         stack 4 locals 4
    31 {
    32                 aload_0;
    33                 dload_1;
    34                 aload_3;
    35                 checkcast       class VIP;
    36                 invokevirtual   Method actionPrice:"(DLjavap/method/VIP;)D";
    37                 dreturn;
    38         
    39 }
    40 
    41 } // end Class VIPOnlyMerchant

    符号引用转为实际引用

      在编译过程中,我们并不知道目标方法的具体内存地址。因此,Java 编译器会暂时用符号引用来表示该目标方法。这一符号引用包括目标方法所在的类或接口的名字,以及目标方法的方法名和方法描述符。

      符号引用存储在 class 文件的常量池之中。根据目标方法是否为接口方法,这些引用可分为接口符号引用和非接口符号引用。我在文章中贴了一个例子,利用“javap -v”打印某个类的常量池,如果你感兴趣的话可以到文章中查看。

    1 // 在奸商.class的常量池中,#16为接口符号引用,指向接口方法"客户.isVIP()"。而#22为非接口符号引用,指向静态方法"奸商.价格歧视()"。
    2 $ javap -v 奸商.class ...
    3 Constant pool:
    4 ...
    5   #16 = InterfaceMethodref #27.#29        // 客户.isVIP:()Z
    6 ...
    7   #22 = Methodref          #1.#33         // 奸商.价格歧视:()D
    8 ...

      上一篇中我曾提到过,在执行使用了符号引用的字节码前,Java 虚拟机需要解析这些符号引用,并替换为实际引用。

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

    1. 在 C 中查找符合名字及描述符的方法。
    2. 如果没有找到,在 C 的父类中继续搜索,直至 Object 类。
    3. 如果没有找到,在 C 所直接实现或间接实现的接口中搜索,这一步搜索得到的目标方法必须是非私有、非静态的。并且,如果目标方法在间接实现的接口中,则需满足 C 与该接口之间没有其他符合条件的目标方法。如果有多个符合条件的目标方法,则任意返回其中一个。

      从这个解析算法可以看出,静态方法也可以通过子类来调用。此外,子类的静态方法会隐藏(注意与重写区分)父类中的同名、同描述符的静态方法。

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

    1. 在 I 中查找符合名字及描述符的方法。
    2. 如果没有找到,在 Object 类中的公有实例方法中搜索。
    3. 如果没有找到,则在 I 的超接口中搜索。这一步的搜索结果的要求与非接口符号引用步骤 3 的要求一致。

      经过上述的解析步骤之后,符号引用会被解析成实际引用。对于可以静态绑定的方法调用而言,实际引用是一个指向方法的指针。对于需要动态绑定的方法调用而言,实际引用则是一个方法表的索引。具体什么是方法表,我会在下一篇中做出解答。

    虚方法调用

     1 package javap.method;
     2 
     3 import java.util.Random;
     4 
     5 public interface Customer {
     6     boolean isVIP();
     7 }
     8 class Merchant {
     9     public double actionPrice(double price, Customer customer) {
    10         return price * 0.8;
    11     }
    12 }
    13 
    14 class NaiveMerchant extends Merchant {
    15     @Override
    16     public double actionPrice(double price, Customer customer) {
    17         if (customer.isVIP()) {        // (4)invokeinterface 调用接口方法
    18             return price * priceDiscri();  // (1)invokestatic 调用静态方法
    19         } else {
    20             /**
    21              * (2)invokespecial:用于调用私有实例方法、构造器,以及使用 super 关键字调用父类的实例方法或构造器,
    22              * 和所实现接口的默认方法
    23              */
    24             return super.actionPrice(price, customer);   // invokespecial (调用super方法)
    25         }
    26     }
    27 
    28     // 价格歧视
    29     public static double priceDiscri() { //咱们的杀熟算法太粗暴了,应该将客户城市作为随机数生成器的种子。
    30         return new Random()      // invokespecial (调用构造方法)
    31                 .nextDouble()    // (3)invokevirtual (调用虚方法-非私有实例方法)
    32                 + 0.8d;
    33     }
    34 
    35     // (5) invokedynamic:用于调用动态方法,比较复杂,此处先不介绍
    36 }

    例子:在实际运行过程中,Java 虚拟机是如何高效地确定每个 Passenger 实例应该去哪条通道的呢?我们一起来看一下。

    abstract class Passenger {
      abstract void passThroughImmigration();
      @Override
      public String toString() { ... }
    }
    class ForeignerPassenger extends Passenger {
       @Override
       void passThroughImmigration() { /* 进外国人通道 */ }
    }
    class ChinesePassenger extends Passenger {
      @Override
      void passThroughImmigration() { /* 进中国人通道 */ }
      void visitDutyFreeShops() { /* 逛免税店 */ }
    }
    
    Passenger passenger = ...
    passenger.passThroughImmigration();
    • 静态绑定(重载-不完全准确)
      • 调用静态方法的 invokestatic 指令 
      • 调用构造器、私有实例方法以及超类非私有实例方法的 invokespecial 指令
    • 动态绑定(重写-不完全准确):如下这些都称为虚函数在绝大多数情况下,Java虚拟机需要在运行时根据调用者的动态类型,来确定虚方法调用的目标方法
      • 所有非私有实例方法调用都会被编译成 invokevirtual 指令
      • 接口方法调用都会被编译成 invokeinterface 指令

      Java 虚拟机中采取了一种用空间换取时间的策略来实现动态绑定。它为每个类生成一张方法表,用以快速定位目标方法。那么方法表具体是怎样实现的呢?

    方法表 

    • 何时生成方法表?类加载的(链接下的准备)准备阶段,它除了为静态字段分配内存之外,还会构造与该类相关联的方法表。
    • 方法表本质上是一个数组:每个数组元素指向一个当前类及其祖先类中非私有的实例方法。这些方法可能是具体的、可执行的方法,也可能是没有相应字节码的抽象方法。
    • 方法表满足两个特质:
      • 其一,子类方法表中包含父类方法表中的所有方法
      • 其二,子类方法在方法表中的索引值,与它所重写的父类方法的索引值相同

      我们知道,方法调用指令中的符号引用会在执行之前解析成实际引用。对于静态绑定的方法调用而言,实际引用将指向具体的目标方法。对于动态绑定的方法调用而言,实际引用则是方法表的索引值(实际上并不仅是索引值)。在执行过程中,Java 虚拟机将获取调用者的实际类型,并在该实际类型的虚方法表中,根据索引值获得目标方法这个过程便是动态绑定。 

     toString 方法和 passThroughImmigration 方法分别对应 0 号和 1 号,是因为 toString 方法的索引值需要与 Object 类中同名方法的索引值一致。为了保持简洁,这里我就不考虑 Object 类中的其他方法。

       实际上,使用了方法表的动态绑定与静态绑定相比,仅仅多出几个内存解引用操作:访问栈上的调用者,读取调用者的动态类型,读取该类型的方法表,读取方法表中某个索引值所对应的目标方法。相对于创建并初始化 Java 栈帧来说,这几个内存解引用操作的开销简直可以忽略不计。那么我们是否可以认为虚方法调用对性能没有太大影响呢?其实是不能的,上述优化的效果看上去十分美好,但实际上仅存在于解释执行中,或者即时编译代码的最坏情况中。这是因为即时编译还拥有另外两种性能更好的优化手段:内联缓存(inlining cache)方法内联(method inlining)- 后续章节再介绍。下面我便来介绍第一种内联缓存 

    内联缓存

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

      如果没有碰到已缓存的类型,一般的做法可能替换单态内联缓存中的纪录。这种做法就好比 CPU 中的数据缓存,它对数据的局部性有要求,即在替换内联缓存之后的一段时间内,方法调用的调用者的动态类型应当保持一致,从而能够有效地利用内联缓存;但是最坏的情况是,每次相邻两个都是未缓存的类型,每次都要更新缓存;上述JVM的做法,与替换内联缓存纪录的做法相比,它牺牲了优化的机会,但是节省了写缓存的额外开销。

    例子:

     1 package javap.method;
     2 
     3 
     4 // Run with: java -XX:CompileCommand='dontinline,*.passThroughImmigration' Passenger
     5 public abstract class Passenger {
     6     abstract void passThroughImmigration();
     7 
     8     public static void main(String[] args) {
     9         Passenger a = new ChinesePassenger();
    10         Passenger b = new ForeignerPassenger();
    11         long current = System.currentTimeMillis();
    12         for (int i = 1; i <= 2000000000; i++) {  // 20亿
    13             if (i % 100000000 == 0) {   // 1亿
    14                 long temp = System.currentTimeMillis();
    15                 System.out.println(temp - current);
    16                 current = temp;
    17             }
    18             Passenger c = (i < 1000000000) ? a : b;  // 10亿
    19             c.passThroughImmigration();
    20         }
    21     }
    22 }
    23 class ChinesePassenger extends Passenger {
    24     @Override void passThroughImmigration() {}
    25 }
    26 class ForeignerPassenger extends Passenger {
    27     @Override void passThroughImmigration() {}
    28 }

    Java 虚拟机中的即时编译器会使用内联缓存来加速动态绑定。Java 虚拟机所采用的单态内联缓存将纪录调用者的动态类型,以及它所对应的目标方法。当碰到新的调用者时,如果其动态类型与缓存中的类型匹配,则直接调用缓存的目标方法。否则,Java 虚拟机将该内联缓存劣化为超多态内联缓存,在今后的执行过程中直接使用方法表进行动态绑定。

    在今天的实践环节,我们来观测一下单态内联缓存和超多态内联缓存的性能差距。为了消除方法内联的影响,请使用如下的命令。

    java -XX:CompileCommand='dontinline,*.passThroughImmigration' javap.method.Passenger,执行4次,每次结果为1列

     1 263 272 256 260  // a开始  -- 单态内联缓存
     2 258 277 269 263
     3 247 257 248 294
     4 247 242 249 312
     5 250 241 249 404
     6 242 263 248 468
     7 258 255 242 364
     8 262 243 242 263
     9 252 271 252 279
    10 246 258 246 249
    11 328 316 296 320  // b开始 -- 劣化为“超多态内联缓存”,在今后的执行过程中直接使用方法表进行动态绑定
    12 315 339 293 369
    13 345 323 286 761
    14 287 314 279 398
    15 294 311 290 349
    16 303 341 289 367
    17 289 345 283 346
    18 282 315 278 363
    19 288 401 277 416
    20 293 388 284 331

    java javap.method.Passenger,执行4次,每次结果为1列 (可能会带来方法内联)

     1 116 83  82  115  // a开始
     2 143 134 128 149
     3 122 141 117 141
     4 131 173 124 128
     5 124 149 131 116
     6 120 153 117 117
     7 115 141 114 114
     8 115 125 116 114
     9 114 116 114 135
    10 113 112 119 132
    11 158 143 144 168 // b开始
    12 140 141 142 156
    13 148 146 145 142
    14 156 153 144 144
    15 142 140 141 144
    16 145 198 307 147
    17 191 216 236 156
    18 157 206 163 144
    19 150 199 152 145
    20 147 191 162 144
  • 相关阅读:
    FNV与FNV-1a Hash算法说明【转】
    FNV哈希算法【转】
    git超详细教程【转】
    Git基础篇【转】
    Notepad++中常用的插件【转】
    美化博客园界面(让您的博客更加赏心悦目)希望对您有用【转】
    scanf()总结--从网上收来的,感觉很好,用来提醒自己,c语言真是博大精深!!【转】
    机器人程序设计——之如何正确入门ROS | 硬创公开课(附视频/PPT)【转】
    ROS学习网址【原创】
    机器人操作系统ROS Indigo 入门学习(1)——安装ROS Indigo【转】
  • 原文地址:https://www.cnblogs.com/wxdlut/p/14023240.html
Copyright © 2011-2022 走看看