考虑这个简单的 Java 类:
class MyClass {
public void bar(MyClass c) {
c.foo();
}
}
我想讨论 c.foo() 行上发生的事情。
原创的、误导性的问题
注意:并非所有这些实际上都发生在每个单独的 invokevirtual 操作码上。提示:如果您想了解 Java 方法调用,请不要只阅读 invokevirtual 的文档!
在字节码级别,c.foo() 的核心将是 invokevirtual 操作码,并且根据 the documentation for invokevirtual ,或多或少会发生以下情况:
- 查找 编译时 类 MyClass 中定义的 foo 方法。 (这涉及首先解析 MyClass。)
- 做一些检查,包括:验证 c 不是初始化方法,并验证调用 MyClass.foo 不会违反任何 protected 修饰符。
- 找出实际调用的方法。特别是,查找 c 的运行时 类型。如果该类型有 foo(),则调用该方法并返回。如果不是,请查找 c 的运行时类型的父类(super class);如果该类型有 foo,则调用该方法并返回。如果不是,查找c的runtime type's superclass's superclass;如果该类型有 foo,则调用该方法并返回。等等。如果找不到合适的方法,则报错。
单独的第 3 步似乎足以确定要调用的方法并验证所述方法是否具有正确的参数/返回类型。所以我的问题是为什么首先要执行第 1 步。可能的答案似乎是:
- 在第 1 步完成之前,您没有足够的信息来执行第 3 步。 (乍一看这似乎难以置信,所以请解释。)
- 在 #1 和 #2 中完成的链接或访问修饰符检查对于防止某些不良事件的发生至关重要,并且这些检查必须基于编译时类型而不是运行时类型层次结构。 (请解释。)
修改后的问题
c.foo() 行的 javac 编译器输出的核心将是这样一条指令:
invokevirtual i
其中 i 是 MyClass 的运行时常量池的索引。该常量池条目将是 CONSTANT_Methodref_info 类型,并将指示(可能间接地)A)被调用方法的名称(即 foo),B)方法签名,以及 C)调用该方法的编译时类的名称在(即 MyClass)上。
问题是,为什么需要引用编译时类型 (MyClass)?既然 invokevirtual 将对 c 的运行时类型进行动态调度,那么存储对编译时类的引用不是多余的吗?
最佳答案
一切都与性能有关。当通过计算出编译时类型(又名:静态类型)时,JVM 可以计算被调用方法在运行时类型(又名:动态类型)的虚函数表中的索引。使用这个索引,第 3 步就变成了对数组的访问,这可以在常数时间内完成。不需要循环。
示例:
class A {
void foo() { }
void bar() { }
}
class B extends A {
void foo() { } // Overrides A.foo()
}
默认情况下,A
扩展了定义这些方法的 Object
( final方法被省略,因为它们是通过 invokespecial
调用的):
class Object {
public int hashCode() { ... }
public boolean equals(Object o) { ... }
public String toString() { ... }
protected void finalize() { ... }
protected Object clone() { ... }
}
现在,考虑这个调用:
A x = ...;
x.foo();
通过确定 x 的静态类型是 A
,JVM 还可以确定在此调用站点可用的方法列表:hashCode
、equals
、toString
、finalize
、clone
、foo
、bar
。在此列表中,foo
是第 6 个条目(hashCode
是第一个,equals
是第二个,依此类推)。此索引计算执行一次 - 当 JVM 加载类文件时。
之后,每当 JVM 处理 x.foo()
时,只需访问 x 提供的方法列表中的第 6 个条目,相当于 x.getClass()。 getMethods[5]
,(如果 x 的动态类型为 A
,则指向 A.foo()
)并调用该方法。无需详尽搜索这一系列方法。
请注意,无论 x 的动态类型如何,方法的索引都保持不变。即:即使 x 指向 B 的实例,第 6 个方法仍然是 foo
(虽然这次它将指向 B.foo()
)。
更新
[根据您的更新]:您是对的。为了执行虚拟方法分派(dispatch),所有 JVM 需要的是方法的名称+签名(或 vtable 中的偏移量)。但是,JVM 不会盲目执行。它首先在名为 verification 的进程中检查加载到其中的 cassfiles 是否正确。 (另请参见 here)。
验证表达了 JVM 的设计原则之一:它不依赖于编译器来产生正确的代码。它会在允许执行代码之前检查代码本身。特别地, validator 检查每个被调用的虚拟方法实际上是由接收者对象的静态类型定义的。显然,需要接收器的静态类型来执行这样的检查。
关于java - 为什么Java的invokevirtual需要解析被调用方法的编译时类?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/2563791/