《深入拆解Java虚拟机》-JVM是如何执行方法调用的?(上)

Posted by 瞿广 on Tuesday, December 25, 2018

TOC

JVM是如何执行方法调用的?(上)

一、重载与重写

同一个类中出现多个名字相同,并且参数类型相同的方法,那么他们的参数类型必须不同。这些方法之间的关系,我们称为重载。

小知识:这个限制可以通过字节码工具绕开。
也就是说,在编译完成之后,我们可以再向 class 文件中添加方法名和参数类型相同,而返回类型不同的方法。
当这种包括多个方法名相同、参数类型相同,而返回类型不同的方法的类,出现在 Java 编译器的用户类路径上时,它是怎么确定需要调用哪个方法的呢?
当前版本的 Java 编译器会直接选取第一个方法名以及参数类型匹配的方法。
并且,它会根据所选取方法的返回类型来决定可不可以通过编译,以及需不需要进行值转换等。

二、JVM 的静态绑定和动态绑定

1.Java虚拟机是怎么识别方法的。

Java虚拟机识别方法的关键在于类名、方法名以及方法描述符(method descirptor) 方法描述符,它是由方法的参数类型以及返回类型所构成。在同一个类中,如果同时出现多个名字相同且描述符也相同的方法,那么Java虚拟机会在类的验证阶段报错。 Java虚拟机中关于方法重写的判断同样基于方法描述符。如果子类定义了与父类中非私有、非静态方法同名的方法,那么只有当这个方法的参数类型以及返回类型一致,Java虚拟机才会判定为重写。

绑定——将一个方法的调用与方法所在的类关联起来。 · 由于对重载方法的区分在编译阶段已经完成,我们可以认为Java虚拟机不存在重载这一概念。在某些文章中,重载也被称为静态绑定(static binding),或者编译时多态(compile-time-polymorphism);而重写则被称为动态绑定(dynamic binding)

2.具体来说,Java字节码中与调用相关的指令共有五种。

  1. invokestatic:用于调用静态方法。
  2. invokespecial:用于调用私有实例方法、构造器,以及使用super关键字调用父类的实例方法或构造器,和所实现接口的默认方法。
  3. invokevirtual:用于调用非私有实例方法。
  4. invokeinterface:用于调用接口方法。
  5. invokedynamic:用于调用动态方法。

    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;
    }
    }
    
    

三、调用指令的符号引用

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

符号引用存储在class文件的常量池之中。根据目标方法是否为接口方法,这些引用可分为接口符号引用和非接口符号引用。

利用“javap -v”打印某个类的常量池

// 在奸商.class 的常量池中,
//#16 为接口符号引用,指向接口方法 " 客户.isVIP()"。
//而 #22 为非接口符号引用,指向静态方法 " 奸商. 价格歧视 ()"。



$ javap -v 奸商.class ...
Constant pool:
...
  #16 = InterfaceMethodref #27.#29        // 客户.isVIP:()Z
...
  #22 = Methodref          #1.#33         // 奸商. 价格歧视:()D
...

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

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

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

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

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

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

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