考虑这个简单的Java类:
class MyClass {
public void bar(MyClass c) {
c.foo();
}
}
我想讨论一下在c.foo()这一行发生了什么。
原本误导性的问题
注意:并不是每个invokevirtual操作码都会实际执行所有这些步骤。提示:如果你想理解Java方法调用,请不要只阅读invokevirtual的文档!
在字节码级别上,c.foo()的核心是invokevirtual操作码。根据invokevirtual的文档,以下是大致的执行过程:
- 查找编译时类MyClass中定义的foo方法。(这涉及到首先解析MyClass。)
- 进行一些检查,包括:验证c不是初始化方法,并验证调用MyClass.foo不会违反任何受保护的修饰符。
- 确定要实际调用哪个方法。特别地,查找c的运行时类型。如果该类型具有foo(),则调用该方法并返回。否则,查找c的运行时类型的超类;如果该类型具有foo,则调用该方法并返回。如果没有,则查找c的运行时类型的超类的超类;如果该类型具有foo,则调用该方法并返回。等等。如果找不到合适的方法,则出错。
仅针对第3步,就足以确定要调用哪个方法并验证所调用的方法具有正确的参数/返回类型。因此,我的问题是为什么需要先执行第1步。可能的答案似乎是:
- 在执行第3步之前,还不具备足够的信息。 (这乍一看似乎不太可能,因此请解释一下。)
- 在第1步和第2步中执行的链接或访问修饰符检查是防止某些糟糕事情发生的关键,且必须基于编译时类型而非运行时类型层次结构进行。 (请解释。)
修订后的问题
c.foo()的javac编译器输出的核心将是这样一条指令:
invokevirtual i
i是指向MyClass的运行时常量池的索引。该常量池条目将是CONSTANT_Methodref_info类型,并且将指示(可能间接地)A)调用的方法名称(即foo),B)方法签名,以及C)在其上调用方法的编译时类的名称(即MyClass)。
问题是,为什么需要引用编译时类型(MyClass)?由于invokevirtual将在c的运行时类型上进行动态分派,存储对编译时类的引用是否重复?