让我们考虑简单的界面:
interface Simple{
void doSth();
}
实现它的两个类:
class A implements Simple{
void someOtherMethod(){ .... }
void doSth(){ ... }
private void doSth(int x){ ... }
}
class B implements Simple{
void methodA(){ ..}
// many other methods
void doSth(){ ... }
private void doSth(Object o, long y){ ... }
}
现在,我可以轻松地写道:
Simple s = new A();
s.doSth();
Java的多态性将完成其余的工作。有谁知道如何Hotspot,确保链接器将链接到正确的方法,考虑到在实现类中可以有更多定义,甚至它们的返回类型可以是原始的子类? Java是否确保接口方法始终以vtable中的某个偏移量开始,例如在0?
答案 0 :(得分:2)
在我们投资之前,让我们简化一下这个例子:
interface Foo {
void bar();
}
class AFoo implements Foo {
int i;
@Override
public void bar() {
i++;
}
}
class AnotherFoo implements Foo {
int i;
@Override
public void bar() {
i--;
}
}
public class Test {
public static void main(String[] args) {
Foo foo = new AFoo();
foo.bar();
}
}
编译完成后,我们使用
javap.exe -verbose Test.class
检查生成的字节码:
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #16 // class tools/AFoo
3: dup
4: invokespecial #18 // Method tools/AFoo."<init>":()V
7: astore_1
8: aload_1
9: invokeinterface #19, 1 // InterfaceMethod tools/Foo.bar:()V
14: return
在类加载时,代码将链接,Java语言规范为specified,如下所示:
类或接口的二进制表示使用其他类和接口的二进制名称(第13.1节)(第13.1节)以符号方式引用其他类和接口及其字段,方法和构造函数。对于字段和方法,这些符号引用包括字段或方法所属的类或接口类型的名称,以及字段或方法本身的名称以及相应的类型信息。
在可以使用符号引用之前,它必须经过解析,其中检查符号引用是正确的,并且通常用直接引用替换,如果重复使用引用,则可以更有效地处理该引用。
请注意,此“直接引用”是指方法的声明。如果有多个实现,则运行时此时不能知道将使用哪种方法。也就是说,在Java语言规范调用链接期间,但在执行实际方法调用表达式时,不会解析多态性。这是Java虚拟机规范的specified:
设C为objectref的类。要调用的实际方法由以下查找过程选择:
如果C包含与已解析方法具有相同名称和描述符的实例方法的声明,则这是要调用的方法,并且查找过程终止。
否则,如果C具有超类,则使用C的直接超类递归地执行相同的查找过程;要调用的方法是递归调用此查找过程的结果。
否则,会引发AbstractMethodError。
由JVM实现如何实际实现它。对于Oracle Hotspot JVM,文档包含rather detailed explanation:
当链接invokeinterface调用时,链接器会在接口中解析对抽象目标方法的调用。这归结为目标接口和该接口中的所谓可用索引。
JVM验证程序永远不会保证目标接口的静态;每个invokeinterface接收器都被键入一个简单的对象引用。因此(与invokevirtual调用不同),不能对接收者的vtable布局做出任何假设。相反,必须更仔细地检查接收者的类(由其_klass字段表示)。如果虚拟调用可以盲目地执行两个或三个间接以到达目标方法,则接口调用必须首先检查接收者的类以确定(a)该类是否实际实现了接口,以及(b)如果是,那么接口的方法记录在该特定类别中。
没有简单的前缀方案,其中接口的方法在实现该接口的每个类中的固定偏移处显示。相反,在通用(非单态)情况下,汇编编码的存根例程必须从接收器的InstanceKlass获取已实现接口的列表,并遍历该列表以寻找当前目标接口。
一旦找到该接口(在接收者的InstanceKlass中),事情变得容易一些,因为接口的方法被安排在一个itable或“接口方法表”中,一个方法的显示,其槽结构对于每个实现有问题的接口的类。因此,一旦在接收者的InstanceKlass中找到接口,关联的偏移量就会将程序集存根指向嵌入InstanceKlass中的itable(就像人们所期望的那样在vtable之后)。此时,调用与虚拟方法调用一样进行。
几乎相同的优化适用于接口调用以及虚拟调用。与虚拟调用一样,大多数接口调用都是单态的,因此可以通过廉价检查呈现为直接调用。
以下是多态接口调用的通用指令跟踪:
callSite:
set #calledInterface, CHECK
call #itableStub[itableSlot]
---
itableStub[itableSlot]:
load (RCVR + #klass), KLASS_TEM
load (KLASS_TEM + #vtableSize), TEM
add (KLASS_TEM + TEM), SCAN_TEM
tryAgain:
# this part is repeated zero or more times, usually zero
load (SCAN_TEM + #itableEntry.interface), TEM
cmp TEM, CHECK
jump,eq foundInterface
test TEM
jump,z noSuchInterface
inc #sizeof(itableEntry), SCAN_TEM
jump tryAgain
tryAgain:
load (SCAN_TEM + #itableEntry.interface), TEM
cmp TEM, CHECK
jump,eq foundInterface
foundInterface:
load (SCAN_TEM + #itableEntry.offset), TEM
load (KLASS_TEM + TEM + #itableSlot), METHOD
load (METHOD + #compiledEntry), TEM
jump TEM
---
compiledEntry:
...
总之,这是六个内存引用和两个非本地跳转。
迂腐:以上所有内容都适用于调用接口方法。调用在类中声明的抽象方法使用different bytecode instruction和稍微简单implementation in the Oracle Hotspot JVM。
答案 1 :(得分:-1)
interface InterfaceA{void method();}
class ClassA implements InterfaceA{void method(){}}
void methodA(InterfaceA[]o){
for(int i=0;i<o.length;++i)o[i].method();
}
void methodB(ClassA[]o){
for(int i=0;i<o.length;++i)o[i].method();
}
所以调用方法比调用方法
慢得多