JVM学习笔记4:方法调用

重载与重写

  • 重载的方法在编译阶段完成识别,调用是根据参数声明类型选择重载方法。选择分三个阶段:

    • 在不考虑对基本类型自动装拆箱(auto-boxing,auto-unboxing),以及可变长参数的情况下选取重载方法;
    • 如果在第 1 个阶段中没有找到适配的方法,那么在允许自动装拆箱,但不允许可变长参数的情况下选取重载方法;
    • 如果在第 2 个阶段中没有找到适配的方法,那么在允许自动装拆箱以及可变长参数的情况下选取重载方法。
  • 如果 Java 编译器在同一个阶段中找到了多个适配的方法,那么它会在其中选择一个最为贴切的,而决定贴切程度的一个关键就是形式参数类型的继承关系。

  • 下面代码中第一次调用会是函数2,因为null可以匹配object和string,但是因为string是Object的子类,所以编译器认为string合适。

1
2
3
4
5
6
7
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 方法
  • 子类中的方法如果满足重载的情况, 是可以与父类方法形成重载的。
  • 子类中方法的定义与父类完全一样则是重写。

JVM 的静态绑定和动态绑定

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

Java 虚拟机中关于方法重写的判定同样基于方法描述符。也就是说,如果子类定义了与父类中非私有、非静态方法同名的方法,那么只有当这两个方法的参数类型以及返回类型一致,Java 虚拟机才会判定为重写。

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

确切地说,Java 虚拟机中的静态绑定指的是在解析时便能够直接识别目标方法的情况,而动态绑定则指的是需要在运行过程中根据调用者的动态类型来识别目标方法的情况。

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

    • invokestatic:用于调用静态方法。
    • invokespecial:用于调用私有实例方法、构造器,以及使用 super 关键字调用父类的实例方法或构造器,和所实现接口的默认方法。
    • invokevirtual:用于调用非私有实例方法。
    • invokeinterface:用于调用接口方法。
    • invokedynamic:用于调用动态方法。
  • 对于 invokestatic 以及 invokespecial 而言,Java 虚拟机能够直接识别具体的目标方法。

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

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

  • 符号引用存储在 class

调用指令的符号引用

  • 文件的常量池之中。根据目标方法是否为接口方法,这些引用可分为接口符号引用和非接口符号引用。
  • 对于可以静态绑定的方法调用而言,实际引用是一个指向方法的指针。对于需要动态绑定的方法调用而言,实际引用则是一个方法表的索引。

虚方法调用

  • Java 里所有非私有实例方法调用都会被编译成 invokevirtual 指令
  • Java 虚拟机需要根据调用者的动态类型,来确定虚方法调用的目标方法。
  • 如果虚方法调用指向一个标记为 final 的方法,那么 Java 虚拟机也可以静态绑定该虚方法调用的目标方法。

方发表

  • 和C++的差不多
  • 方法表满足两个特质:其一,子类方法表中包含父类方法表中的所有方法;其二,子类方法在方法表中的索引值,与它所重写的父类方法的索引值相同。
  • 使用了方法表的动态绑定与静态绑定相比,仅仅多出几个内存解引用操作:访问栈上的调用者,读取调用者的动态类型,读取该类型的方法表,读取方法表中某个索引值所对应的目标方法。相对于创建并初始化 Java 栈帧来说,这几个内存解引用操作的开销简直可以忽略不计。

内联缓存

  • 内联缓存是一种加快动态绑定的优化技术。
  • 它能够缓存虚方法调用中调用者的动态类型,以及该类型所对应的目标方法。在之后的执行过程中,如果碰到已缓存的类型,内联缓存便会直接调用该类型所对应的目标方法。如果没有碰到已缓存的类型,内联缓存则会退化至使用基于方法表的动态绑定。
  • Java 虚拟机中的即时编译器会使用内联缓存来加速动态绑定。开始先采用的单态内联缓存,当碰到新的调用者时,如果其动态类型与缓存中的类型匹配,则直接调用缓存的目标方法。否则,Java 虚拟机将该内联缓存劣化为超多态内联缓存,在今后的执行过程中直接使用方法表进行动态绑定。

单态(monomorphic)指的是仅有一种状态的情况。

多态(polymorphic)指的是有限数量种状态的情况。二态(bimorphic)是多态的其中一种。

超多态(megamorphic)指的是更多种状态的情况。通常我们用一个具体数值来区分多态和超多态。在这个数值之下,我们称之为多态。否则,我们称之为超多态。

# Java
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×