JVM方法调用的静态绑定和动态绑定

393 阅读7分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第9天,点击查看活动详情

本系列专栏:JVM专栏

前言

上一篇文章,我们说了Java语言和JVM对于重载和重写是不一样的,并且Java编译器通过生成桥接方法来弥补这一不同点,那本章内容就直接来说Java字节码中不同的方法调用是如何区分的。

正文

静态绑定和动态绑定

对于不同的方法,前面说了有的方法在解析Java代码时便能够识别目标方法,而有的方法必须要在运行时根据调用者的动态类型来识别目标方法,比如上一篇文章说的重写的情况,只有在运行时,发现调用类型是Student,这时才可以去调用Student中的重写方法(而不是People中的方法),前者就是静态绑定,而后者就是动态绑定

所以根据逻辑,Java字节码中关于方法调用的一共有5种:

  • invokestatic:用于调用静态方法。这种方法有什么特点呢,调用它是不需要类的实例的,即在操作栈中,只需要弹出该静态方法所需的参数即可。

  • invokespecial:用于调用私有实例方法、构造器,以及使用super关键字调用父类的实例方法或者构造器,和所实现接口的默认方法。这种方法的特点就是能够直接找到,不需要运行时才能找到目标方法。

  • invokevirtual:用于调用非私有实例方法。这种方法就存在被重写的情况,只能等到运行时才能找到目标方法。

  • invokeinterface:用于调用接口方法。同样这种方法也需要等到运行时,才能找到目标方法。

  • invokedynamic:用于调用动态方法,该调用比较复杂,后面单独再说。

由定义可知,这里的invokevertual和invokeinterface需要在JVM运行过程中,根据调用者的动态类型来确定目标调用方法

简单例子

我们还是通过一个简单的例子,来验证一下上面所说的调用指令,是否符合预期。

下面定义了3个类:

//客户接口,是否是VIP
public interface ICustom {

    boolean isVIP();
}
//正常超市
public class Market {

    //打折价格
    public double OffPrice(double price,ICustom customer){
        return  price * 0.8;
    }
}
//假超市,会对VIP进行特殊加价
public class FakeMarket extends Market{

    //判断是否是VIP
    public double OffPrice(double price,ICustom customer){
        if (customer.isVIP()){
            return fakeOffPrice(price);
        }else {
            return super.OffPrice(price,customer);
        }
    }

    //价格更高
    public static double fakeOffPrice(double price){
        return price * 0.9;
    }

}

上面定义了一个简单的逻辑,可以进行查看,我们来看一下FakeMarket类的字节码:

public class com.wayeal.arithmeticapp.testInvoke.FakeMarket extends com.wayeal.arithmeticapp.testInvoke.Market
  minor version: 0
  major version: 55
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #7                          // com/wayeal/arithmeticapp/testInvoke/FakeMarket
  super_class: #8                         // com/wayeal/arithmeticapp/testInvoke/Market
  interfaces: 0, fields: 0, methods: 3, attributes: 1
Constant pool:
   #1 = Methodref          #8.#20         // com/wayeal/arithmeticapp/testInvoke/Market."<init>":()V
   #2 = InterfaceMethodref #21.#22        // com/wayeal/arithmeticapp/testInvoke/ICustom.isVIP:()Z
   #3 = Methodref          #7.#23         // com/wayeal/arithmeticapp/testInvoke/FakeMarket.fakeOffPrice:(D)D
   #4 = Methodref          #8.#24         // com/wayeal/arithmeticapp/testInvoke/Market.OffPrice:(DLcom/wayeal/arithmeticapp/testInvoke/ICustom;)D
   #5 = Double             0.9d
   #7 = Class              #25            // com/wayeal/arithmeticapp/testInvoke/FakeMarket
   #8 = Class              #26            // com/wayeal/arithmeticapp/testInvoke/Market
   #9 = Utf8               <init>
  #10 = Utf8               ()V
  #11 = Utf8               Code
  #12 = Utf8               LineNumberTable
  #13 = Utf8               OffPrice
  #14 = Utf8               (DLcom/wayeal/arithmeticapp/testInvoke/ICustom;)D
  #15 = Utf8               StackMapTable
  #16 = Utf8               fakeOffPrice
  #17 = Utf8               (D)D
  #18 = Utf8               SourceFile
  #19 = Utf8               FakeMarket.java
  #20 = NameAndType        #9:#10         // "<init>":()V
  #21 = Class              #27            // com/wayeal/arithmeticapp/testInvoke/ICustom
  #22 = NameAndType        #28:#29        // isVIP:()Z
  #23 = NameAndType        #16:#17        // fakeOffPrice:(D)D
  #24 = NameAndType        #13:#14        // OffPrice:(DLcom/wayeal/arithmeticapp/testInvoke/ICustom;)D
  #25 = Utf8               com/wayeal/arithmeticapp/testInvoke/FakeMarket
  #26 = Utf8               com/wayeal/arithmeticapp/testInvoke/Market
  #27 = Utf8               com/wayeal/arithmeticapp/testInvoke/ICustom
  #28 = Utf8               isVIP
  #29 = Utf8               ()Z
{
  public com.wayeal.arithmeticapp.testInvoke.FakeMarket();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method com/wayeal/arithmeticapp/testInvoke/Market."<init>":()V
         4: return
      LineNumberTable:
        line 4: 0

  public double OffPrice(double, com.wayeal.arithmeticapp.testInvoke.ICustom);
    descriptor: (DLcom/wayeal/arithmeticapp/testInvoke/ICustom;)D
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=4, locals=4, args_size=3
         0: aload_3
         1: invokeinterface #2,  1            // InterfaceMethod com/wayeal/arithmeticapp/testInvoke/ICustom.isVIP:()Z
         6: ifeq          14
         9: dload_1
        10: invokestatic  #3                  // Method fakeOffPrice:(D)D
        13: dreturn
        14: aload_0
        15: dload_1
        16: aload_3
        17: invokespecial #4                  // Method com/wayeal/arithmeticapp/testInvoke/Market.OffPrice:(DLcom/wayeal/arithmeticapp/testInvoke/ICustom;)D
        20: dreturn
      LineNumberTable:
        line 8: 0
        line 9: 9
        line 11: 14
      StackMapTable: number_of_entries = 1
        frame_type = 14 /* same */

  public static double fakeOffPrice(double);
    descriptor: (D)D
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=4, locals=2, args_size=1
         0: dload_0
         1: ldc2_w        #5                  // double 0.9d
         4: dmul
         5: dreturn
      LineNumberTable:
        line 17: 0
}

相信大家如果阅读过前面文章,对于字节码的阅读已经不费事了,我们直接来看OffPrice方法,其中#1行代码:

 1: invokeinterface #2,  1            // InterfaceMethod com/wayeal/arithmeticapp/testInvoke/ICustom.isVIP:()Z

这里是调用接口的isVIP方法,发现这里是通过invokeinterface方法调用,对于#10行代码:

10: invokestatic  #3                  // Method fakeOffPrice:(D)D

这里是调用类自己的静态方法,使用invokestatic指令,对于#17行代码:

 17: invokespecial #4                  // Method com/wayeal/arithmeticapp/testInvoke/Market.OffPrice:(DLcom/wayeal/arithmeticapp/testInvoke/ICustom;)D

调用父类的方法,这里是通过super来调用,根据规则使用invokespecial指令。

可以发现这里调用的指令和我们预期是相符的。

调用指令的符号引用

在前面文章介绍字节码时,我们说过在字节码的常量池中保存的是常量值和符号引用,对于方法而言,在编译过程中,我们并不知道目标方法的具体内存地址,因此Java编译器会暂时用符号引用来表示目标方法

法的符号引用就包括目标方法所在的类或者接口的名字,以及目标方法的方法名和方法描述符,比如下面方法的符号引用:

 #2 = InterfaceMethodref #21.#22        // com/wayeal/arithmeticapp/testInvoke/ICustom.isVIP:()Z
   #3 = Methodref          #7.#23         // com/wayeal/arithmeticapp/testInvoke/FakeMarket.fakeOffPrice:(D)D
   #4 = Methodref          #8.#24         // com/wayeal/arithmeticapp/testInvoke/Market.OffPrice:(DLcom/wayeal/arithmeticapp/testInvoke/ICustom;)D

那么在前面加载class文章里我们说过,在执行步骤前,要对这些符号引用进行解析,并且替换为实际引用,那JVM如何去查找呢。

非接口符号引用

假设一个符号引用所指向的类为C,则JVM会按照下面步骤进行查找:

  • 在C中查找符合名字以及描述符的方法。

  • 如果没有找到,在C的父类中继续搜索,知道Object类。

  • 如果没有找到,在C所直接实现或者间接实现的接口中搜索,这一步搜索得到的目标方法必须是非私有、非静态的

从这个解析算法可以看出静态方法也可以通过子类来调用,因此子类的静态方法会隐藏父类中同名、同描述符的静态方法。

接口符号引用

假设一个符号引用所指向的接口为I,则JVM会按照下面步骤查找:

  • 在I中查找符合名字以及描述符的方法。

  • 如果没有找到,在Object类中的公有实例方法中搜索。

  • 如果没有找到,则在I的超接口中搜索,同样是非私有、非静态的

经过上述解析步骤后,符号引用就会被解析为实际引用。对于可以静态绑定的方法调用而言,实际引用是一个指向方法的指针。对于需要动态绑定的方法调用而言,实际引用则是一个方法表的索引。具体什么是方法表,我们在下篇文章仔细来说明。

具体来说就是对于invokestatic和invokespecial能找到对应方法的指针,而invokevirtual和invokeinterface找到的是方法表中的一个索引。

总结

本节内容主要是2个部分,重点介绍了因为Java语言和JVM对于重写的定义不一样导致的方法调用也不一样,而对于常见的4种方法调用也分为2种,静态绑定在解析阶段就可以找到实际方法的指针,而动态绑定只能指向一个方法表的索引,在运行时才能确定具体调用的方法。