Chr*_*ris 11 java methods jvm virtual-functions
考虑这个简单的Java类:
class MyClass {
public void bar(MyClass c) {
c.foo();
}
}
Run Code Online (Sandbox Code Playgroud)
我想讨论一下c.foo()行会发生什么.
原创,误导性问题
注意:并非所有这些实际上都发生在每个invokevirtual操作码上.提示:如果您想了解Java方法调用,请不要只阅读invokevirtual的文档!
在字节码级别,c.foo()的内容将是invokevirtual操作码,并且根据invokevirtual 的文档,或多或少会发生以下情况:
单独的步骤#3似乎足以确定调用哪个方法并验证所述方法具有正确的参数/返回类型.所以我的问题是为什么第一步执行第一步.可能的答案似乎是:
修订问题
行c.foo()的javac编译器输出的核心将是这样的指令:
invokevirtual i
Run Code Online (Sandbox Code Playgroud)
其中i是MyClass的运行时常量池的索引.该常量池条目的类型为CONSTANT_Methodref_info,并将指示(可能是间接的)A)调用方法的名称(即foo),B)方法签名,以及C)调用该方法的编译时类的名称on(即MyClass).
问题是,为什么需要编译时类型(MyClass)的引用?由于invokevirtual将在c的运行时类型上进行动态调度,因此将引用存储到编译时类是不是多余的?
这一切都与性能有关。当通过计算出编译时类型(又名:静态类型)时,JVM 可以计算出被调用方法在运行时类型(又名:动态类型)的虚函数表中的索引。使用这个索引,第 3 步就变成了对数组的访问,可以在常数时间内完成。不需要循环。
例子:
class A {
void foo() { }
void bar() { }
}
class B extends A {
void foo() { } // Overrides A.foo()
}
Run Code Online (Sandbox Code Playgroud)
默认情况下,A扩展Object定义了这些方法(最终方法被省略,因为它们是通过调用的invokespecial):
class Object {
public int hashCode() { ... }
public boolean equals(Object o) { ... }
public String toString() { ... }
protected void finalize() { ... }
protected Object clone() { ... }
}
Run Code Online (Sandbox Code Playgroud)
现在,考虑这个调用:
A x = ...;
x.foo();
Run Code Online (Sandbox Code Playgroud)
通过找出 x 的静态类型,AJVM 还可以找出此调用站点可用的方法列表:hashCode, equals, toString, finalize, clone, foo, bar。在此列表中,foo是第 6 个条目(hashCode第 1 个、equals第 2 个等)。索引的计算只执行一次 - 当 JVM 加载类文件时。
之后,每当 JVM 处理时,x.foo()只需访问 x 提供的方法列表中的第 6 个条目,相当于x.getClass().getMethods[5], (它指向A.foo()x 的动态类型是否为A)并调用该方法。无需详尽地搜索这一系列方法。
请注意,无论 x 的动态类型如何,该方法的索引都保持不变。即:即使x指向B的实例,第6个方法仍然是foo(虽然这次它会指向B.foo())。
更新
[根据您的更新]:您是对的。为了执行虚拟方法分派,JVM 需要的只是方法的名称+签名(或 vtable 内的偏移量)。然而,JVM 不会盲目地执行事情。它首先在称为验证的过程中检查加载到其中的 casfile 是否正确(另请参阅此处)。
验证表达了 JVM 的设计原则之一:它不依赖编译器来产生正确的代码。它在允许代码执行之前检查代码本身。特别是,验证器检查每个调用的虚拟方法实际上是由接收者对象的静态类型定义的。显然,需要接收者的静态类型来执行这样的检查。
| 归档时间: |
|
| 查看次数: |
3503 次 |
| 最近记录: |