Java 虚拟机是如何执行方法调用的

JVM 的静态绑定和动态绑定

Java 虚拟机识别方法的关键在于类名、方法名以及方法描述符(method descriptor)。其中方法描述符,它是由方法的参数类型以及返回类型所构成。在同一个类中,如果同时出现多个名字相同且描述也相同的方法,那么 Java 虚拟机会在类的验证阶段报错。

其实 Java 虚拟机与 Java 语言不同,它并不限制名字与参数类型相同,但返回类型不同的方法出现在同一个类中,对于调用这些方法的字节码来说,由于字节码所附带的方法描述符包含了返回类型,因此 Java 虚拟机能够准确地识别目标方法。

Java 虚拟机中关于方法重写地判定同样基于方法描述符。也就是说,如果子类定义了与父类中非私有、非静态方法同名地方法,那么只有当这两个方法地参数类型以及返回类型一致,Java 虚拟机才会判定为重写。对于 Java 语言中重写而 Java 虚拟机中非重写地情况,编译器会通过生成桥接方法来实现 Java 中地重写语义。

由于对重载方法地区分在编译阶段已经完成,可以认为 Java 虚拟机不存在重载这一概念。因此,有些文章里重载也被称为静态绑定(static binding),或者编译时多态,而重写则被称为动态绑定(dynamic binding)。

但这个说法在 Java 虚拟机下并非完全正确。因为某个类地重载方法可能被它地子类所重写,因此 Java 编译器会将所有对非私有实例方法地调用编译为需要动态绑定地类型。即,Java 虚拟机中地静态绑定则指地是需要在解析时便能够直接识别目标方法地情况,而动态绑定则指需要在运行过程中根据调用者地动态类型来识别目标方法地情况。

具体,Java 字节码中与调用相关地指令共有五种。

  1. invokestatic :用于调用静态方法
  2. invokespecial:用与调用滴油实例方法、构造器、使用super关键字调用父类地实例方法或构造器,和实现接口地默认方法。
  3. invokevirtual:用于调用非私有实例方法
  4. involeinterface:用于调用接口方法
  5. involedynamic:用于调用动态方法。
    使用:
interface 客户 {
  boolean isVIP();
}
 
class 商户 {
  public double 折后价格 (double 原价, 客户 某客户) {
    return 原价 * 0.8d;
  }
}
 
class 奸商 extends 商户 {
  @Override
  public double 折后价格 (double 原价, 客户 某客户) {
    if (某客户.isVIP()) {                         // invokeinterface      
      return 原价 * 价格歧视 ();                    // invokestatic
    } else {
      return super. 折后价格 (原价, 某客户);          // invokespecial
    }
  }
  public static double 价格歧视 () {
    // 咱们的杀熟算法太粗暴了,应该将客户城市作为随机数生成器的种子。
    return new Random()                          // invokespecial
           .nextDouble()                         // invokevirtual
           + 0.8d;
  }
}

在代码中,“商户”类定义了一个成员方法,叫做“折后价格”,它将接收一个 double 类型的参数,以及一个“客户”类型的参数。这里“客户”是一个接口,它定义了一个接口方法,叫“isVIP”。还定义了另一个叫做“奸商”的类,它继承了“商户”类,并且重写了“折后价格”这个方法。如果客户是 VIP,那么它会被给到一个更低的折扣。在这个方法中,我们首先会调用“客户”接口的”isVIP“方法。该调用会被编译为 invokeinterface 指令。

如果客户是 VIP,那么我们会调用奸商类的一个名叫“价格歧视”的静态方法。该调用会被编译为 invokestatic 指令。如果客户不是 VIP,那么我们会通过 super 关键字调用父类的“折后价格”方法。该调用会被编译为 invokespecial 指令。

在静态方法“价格歧视”中,我们会调用 Random 类的构造器。该调用会被编译为 invokespecial 指令。然后我们会以这个新建的 Random 对象为调用者,调用 Random 类中的 nextDouble 方法。该调用会被编译为 invokevirutal 指令。

对于 invokestatic 以及 invokespecial 而言,Java 虚拟机能够直接识别具体的目标方法。
而对于 invokevirtual 以及 invokeinterface 而言,在绝大部分情况下,虚拟机需要在执行过程中,根据调用者的动态类型,来确定具体的目标方法。

唯一的例外在于,如果虚拟机能够确定目标方法有且仅有一个,比如说目标方法被标记为 final[3][4],那么它可以不通过动态类型,直接确定目标方法。

调用指令地符号引用

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

符号引用存储在 class 文件地常量池中。根据目标方法是否为接口方法,这些引用可分为接口符号引用和非接口符号引用。在执行使用符号引用地字节码前,Java 虚拟机需要解析这些符号引用,并替换为实际引用。对于非接口符号引用,假定该符号引用所指向地类为C,则Java虚拟机会按照如下步骤查找:

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

以上,静态方法也可以通过子类来调用。此外,子类的静态方法会隐藏(注意与重写区分)父类中同名、同描述的静态方法。对于接口符号引用,假定该符号引用所指向的接口为 I,则 Java 虚拟机会按照如下步骤查找:

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

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

小结

Java 虚拟机识别方法的方式略有不同,除了方法名和参数类型之外,它还会考虑返回类型。

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

在 class 文件中,Java 编译器会用符号引用指代目标方法。在执行调用指令前,它所附带的符号引用需要被解析成实际引用。对于可以静态绑定的方法调用而言,实际引用为目标方法的指针。对于需要动态绑定的方法调用而言,实际引用为辅助动态绑定的信息。

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

猜你喜欢

转载自blog.csdn.net/qq_40488936/article/details/106353378