一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第10天,点击查看活动详情
本系列专栏:JVM专栏
前言
上一篇文章留了个坑,也就是动态绑定时,比如invokevirtual和invokeinterface指令是如何找到其明白方法指针的,说是使用方法表,那本篇文章就来仔细说明一番。
正文
虚方法
这里涉及到一个新概念叫做虚方法,啥是虚方法呢,简单来说能别子类重写的方法。假如接口I中有个方法funC,这时类A、B、C都实现了接口I,这时接口I中的funC就是虚方法,这时定义一个类型为I的实例i,调用i.funC时,必须在运行时获取运行时的类型是A、B还是C,才知道真正要调用是哪个方法。
是不是可以看出这个逻辑就对应于前面所说的invokevirtual和invokeinterface这2个指令,而这均属于虚方法调用,也就是动态绑定,JVM采用了一种用空间换取时间的策略来实现这个动态绑定。
JVM为每个类生成一张方法表,用以快速定位目标方法。
方法表
方法表在类加载的准备阶段创建的,在准备阶段之前文章说过,它除了会为静态字段分配内存之外,还会构造与该类相关联的方法表。
方法表的本质是一个数组,每个数组元素指向一个当前类以及其祖先类中非私有的实例方法。
这些方法可能是具体、可执行的方法,也可能是没有相应字节码的抽象方法,方法表满足2个特征:
-
子类方法表中包含父类方发表中的所有方法。
-
子类方法在方法表中的索引值,与它所重写的父类方法的索引值相同。
然后动态绑定时,方法的实际引用就是方发表的索引值。
这里说概念可能太抽象,我们来看个简单例子,下面是java代码:
//抽象类
abstract class Passenger {
abstract void passThroughImmigration();
@Override
public String toString() { ... }
}
//外国人类,实现类1
class ForeignerPassenger extends Passenger {
@Override
void passThroughImmigration() { /* 进外国人通道 */ }
//中国人类,实现类2
class ChinesePassenger extends Passenger {
@Override
void passThroughImmigration() { /* 进中国人通道 */ }
void visitDutyFreeShops() { /* 逛免税店 */ }
}
//抽象类类型实例
Passenger passenger = ...
passenger.passThroughImmigration();
上面定义了一个抽象父类,和2个实现类,其中ChinesePassenger类中多个方法,在运行时,定义Passenger类型的实例,然后调用passThroughImmigration方法,这时也不确定会调用哪个子类的方法,JVM会生成如下的方法表:
可以看到子类方发表拥有父类方发表所有的方法,而且子类拥有父类父类方法时,其索引是一样的。
那有了这个方法表,是如何进行动态绑定的呢,也就仅仅多了几个内存解引用操作:访问栈上的调用者,读取调用者的动态类型,获取该类型的方法表,读取方法表中某个索引值对应的目标方法,而这个开销也不高。
比如还是这个代码:
Passenger passenger = ...
passenger.passThroughImmigration();
passenger类型是Passenger,调用的方法的方法索引就是方发表中的1,然后 ... 部分会new 出一个具体类型X实例,当调用passThroughImmigration方法时,获取栈顶引用的动态类型X,这时会调用X的方法表的1的方法。
解惑
其实上面例子有一点不好,就是在编译期就知道了动态类型,假如有下面伪代码:
A a = null;
int i = 随机数;
if(i 是奇数){
a = new B();
}else{
a = new C();
}
a.fun();
假如B和C都是A的子类,这样就无法在编译期知道a的动态类型了。也更好理解上面的方发表动态绑定过程。
总结
Java的虚方法调用过程,其实就是理解Java多态的过程,而Java多态是Java的重要特征之一,JVM采用了一种以空间换时间的方法即方法表来完成动态绑定。