绑定过程
静态绑定与动态绑定
-
静态绑定:
指在解析阶段就能够直接识别到目标方法的情况。
(重载可以看成静态绑定,因为对于重载方法的区分在编译阶段就已经完成,所以也可以认为Java虚拟机中不存在重载这一概念) -
动态绑定:
指需要在运行过程中根据调用者的动态类型来识别目标方法的情况。
(重写也可被称为动态绑定) -
在 class 文件中,编译器会用符号引用指代目标方法。在执行调用之类前,其符号引用会被解析成实际引用。
-
对于静态绑定的方法调用来说,实际引用为直接指向目标方法的指针。
-
对于动态绑定的方法调用而言,实际引用为一个方法表的索引。
符号引用解析成实际引用的过程
符号引用存储在 class 文件的常量池之中。根据目标方法是否为接口方法,这些引用可分为接口符号引用和非接口符号引用。
在执行使用了符号引用的字节码前,Java 虚拟机需要解析这些符号引用,并替换为实际引用。
-
非接口符号引用:
1)在 目标类中查找符合名字及描述符的方法。
2)如果没有找到,在目标类的父类中继续搜索,直至 Object 类。
3)如果没有找到,在目标类所直接实现或间接实现的接口中搜索,这一步搜索得到的目标方法必须是非私有、非静态的。并且,如果目标方法在间接实现的接口中,则需满足目标类与该接口之间没有其他符合条件的目标方法。如果有多个符合条件的目标方法,则任意返回其中一个。 -
接口符号引用:
1)在 I 中查找符合名字及描述符的方法。
2)如果没有找到,在 Object 类中的公有实例方法中搜索。
3)如果没有找到,则在 I 的超接口中搜索。这一步的搜索结果的要求与非接口符号引用步骤 3 的要求一致。
调用过程
对于静态绑定的方法调用而言,实际引用将指向具体的目标方法。对于动态绑定的方法调用而言,实际引用则是方法表的索引值(实际上并不仅是索引值)。
调用指令
- invokestatic:用于调用静态方法。
- invokespecial:用于调用私有实例方法、构造器,以及使用 super 关键字调用父类的实例方法或构造器,和所实现接口的默认方法。
- invokevirtual:用于调用非私有实例方法。
- invokeinterface:用于调用接口方法。
- invokedynamic:用于调用动态方法。
非虚方法调用(静态绑定)
在 Java 虚拟机中,静态绑定包括用于调用静态方法的 invokestatic 指令,和用于调用构造器、私有实例方法以及超类非私有实例方法的 invokespecial 指令。如果虚方法调用指向一个标记为 final 的方法,那么 Java 虚拟机也可以静态绑定该虚方法调用的目标方法。
方法表
- Java 虚拟机中采取了一种用空间换取时间的策略来实现动态绑定。它为每个类生成一张方法表,用以快速定位目标方法。
- 方法表本质上是一个数组,每个数组元素指向一个当前类及其祖先类中非私有的实例方法。
- 方法表满足两个特质:其一,子类方法表中包含父类方法表中的所有方法;其二,子类方法在方法表中的索引值,与它所重写的父类方法的索引值相同。
- 在执行过程中,Java 虚拟机将获取调用者的实际类型,并在该实际类型的虚方法表中,根据索引值获得目标方法。这个过程便是动态绑定。
虚方法调用(动态绑定)
Java 里所有非私有实例方法调用都会被编译成 invokevirtual 指令,而接口方法调用都会被编译成 invokeinterface 指令。这两种指令,均属于 Java 虚拟机中的虚方法调用。
- 虚方法调用包括 invokevirtual 指令和 invokeinterface 指令。如果这两种指令所声明的目标方法被标记为 final,那么 Java 虚拟机会采用静态绑定。否则,Java 虚拟机将采用动态绑定,在运行过程中根据调用者的动态类型,来决定具体的目标方法。
- Java 虚拟机的动态绑定是通过方法表这一数据结构来实现的。方法表中每一个重写方法的索引值,与父类方法表中被重写的方法的索引值一致。
- 在解析虚方法调用时,Java 虚拟机会纪录下所声明的目标方法的索引值,并且在运行过程中根据这个索引值查找具体的目标方法。
- Java 虚拟机中的即时编译器会使用内联缓存来加速动态绑定。Java 虚拟机所采用的单态内联缓存将纪录调用者的动态类型,以及它所对应的目标方法。当碰到新的调用者时,如果其动态类型与缓存中的类型匹配,则直接调用缓存的目标方法。否则,Java 虚拟机将该内联缓存劣化为超多态内联缓存,在今后的执行过程中直接使用方法表进行动态绑定。