7

Java 中的动态绑定发生在运行时,用于覆盖函数。我想知道它是如何在内部发生的(比如在 C++ 中,使用了虚函数/表)。

4

3 回答 3

5

Java 中的所有非最终非私有、非静态方法都是虚拟的

于 2013-08-06T14:15:36.320 回答
4

如上所述,在 Java 中,所有非最终非私有、非静态方法都是虚拟的,这意味着可以在子类中覆盖的任何方法都是虚拟的。

但是为了理解 JVM 如何在内部处理它,让我们看下面的代码示例

public class OverridingInternalExample {

    private static class Mammal {
        public void speak() { System.out.println("ohlllalalalalalaoaoaoa"); }
    }

    private static class Human extends Mammal {

        @Override
        public void speak() { System.out.println("Hello"); }

        // Valid overload of speak
        public void speak(String language) {
            if (language.equals("Hindi")) System.out.println("Namaste");
            else System.out.println("Hello");
        }

        @Override
        public String toString() { return "Human Class"; }

    }

    //  Code below contains the output and and bytecode of the method calls
    public static void main(String[] args) {
        Mammal anyMammal = new Mammal();
        anyMammal.speak();  // Output - ohlllalalalalalaoaoaoa
        // 10: invokevirtual #4 // Method org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V

        Mammal humanMammal = new Human();
        humanMammal.speak(); // Output - Hello
        // 23: invokevirtual #4 // Method org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V

        Human human = new Human();
        human.speak(); // Output - Hello
        // 36: invokevirtual #7 // Method org/programming/mitra/exercises/OverridingInternalExample$Human.speak:()V

        human.speak("Hindi"); // Output - Namaste
        // 42: invokevirtual #9 // Method org/programming/mitra/exercises/OverridingInternalExample$Human.speak:(Ljava/lang/String;)V
    }
}

anyMammal.speak()我们可以看到和的字节码humanMammal.speak()是相同的(invokevirtual #4 // Method org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V),因为根据编译器,这两种方法都是在哺乳动物参考中调用的。

所以现在问题来了,如果两个方法调用具有相同的字节码,那么 JVM 是如何知道调用哪个方法的呢?

invokevirtual好吧,根据 JVM 规范 ,答案隐藏在字节码本身中,它是指令

invokevirtual 调用对象的实例方法,在对象的(虚拟)类型上进行调度。这是 Java 编程语言中的常规方法分派。

JVM 使用该invokevirtual指令调用与 C++ 虚拟方法等效的 Java。在 C++ 中,如果我们要覆盖另一个类中的一个方法,我们需要将其声明为virtual,但在 Java 中,所有方法默认情况下都是虚拟的(除了 final 和静态方法),因为我们可以覆盖子类中的每个方法。

操作invokevirtual接受指向方法引用调用的指针(#4常量池的索引)

invokevirtual #4   // Method org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V

并且该方法引用 #4 再次引用方法名称和类引用

#4 = Methodref   #2.#27   // org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V
#2 = Class   #25   // org/programming/mitra/exercises/OverridingInternalExample$Mammal
#25 = Utf8   org/programming/mitra/exercises/OverridingInternalExample$Mammal
#27 = NameAndType   #35:#17   // speak:()V
#35 = Utf8   speak
#17 = Utf8

所有这些引用共同用于获取对方法和要在其中找到该方法的类的引用。JVM规范中也提到了这一点

Java 虚拟机不要求对象 4 有任何特定的内部结构。

书签4状态

在 Oracle 的一些 Java 虚拟机实现中,对类实例的引用是指向句柄的指针,句柄本身就是一对指针:一个指向包含对象方法的表,另一个指向代表对象的类型,另一个是从堆中为对象数据分配的内存。

这意味着每个引用变量都包含两个隐藏指针

  1. 指向再次保存对象方法的表的指针和指向 Class 对象的指针。例如 [speak(), speak(String) 类对象]
  2. 指向在堆上为该对象的数据分配的内存的指针,例如实例变量的值。

但问题又来了,invokevirtual内部如何做到这一点?好吧,没有人能回答这个问题,因为它取决于 JVM 的实现,并且因 JVM 而异。

从上面的陈述中,我们可以得出结论,一个对象引用间接持有一个引用/指针,指向一个包含该对象的所有方法引用的表。Java 从 C++ 借用了这个概念,这个表有各种名称,例如虚拟方法表 ( VMT )、虚拟函数表 ( vftable )、虚拟表 ( vtable )、调度表

我们无法确定vtable在 Java 中是如何实现的,因为它依赖于 JVM。但我们可以预期它将遵循与 C++ 相同的策略,其中vtable是一个类似数组的结构,它保存方法名称及其对数组索引的引用。每当 JVM 尝试执行一个虚拟方法时,它总是会询问vtable它的地址。

每个类只有一个vtable意味着它对于与对象相似的类的所有对象都是唯一且相同的 Class。我Class在我的文章Why an external Java class can't be staticWhy Java is Purely Object-Oriented Language Or Why Not中讨论了更多关于对象的内容。

所以只有一个vtableObject包含所有 11 种方法(如果我们不计算 registerNatives)和对它们各自方法体的引用。

vtable-of-object

当 JVM 将Mammal类加载到内存中时,它会Class为它创建一个对象并创建一个对象,该对象vtable包含 Object 类的 vtable 中具有相同引用的所有方法(因为Mammal没有覆盖 Object 的任何方法),并为speak方法添加一个新条目。

vtable-人类

现在轮到类了,现在 JVM 会将类中的Human所有条目复制到vtableof中,并为重载版本的. MammalvtableHumanspeak(String)

JVM 知道Human该类已经覆盖了两个方法,一个是toString()from Object,第二个是speck()from Mammal。现在,而不是使用更新的引用为这些方法创建新条目。JVM 将修改对已存在方法的引用,这些引用位于它们之前存在的同一索引上,并将保持相同的方法名称。

invokevirtual导致 JVM 将方法引用处的值视为在当前对象 #4中查找的方法的名称,而不是地址。vtable

我希望现在已经有点清楚 JVM 如何混合constant pool条目并vtable得出结论它将调用哪个方法。

您可以阅读我的文章JVM 如何在内部处理方法重载和覆盖

于 2018-10-10T05:31:50.303 回答
0

JVM 规范在此处描述了 JVM 必须如何解析虚拟方法:http: //docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.invokevirtual

这部分是最相关的:

否则,如果 C 具有超类,则使用 C 的直接超类递归地执行相同的查找过程;要调用的方法是此查找过程的递归调用的结果。

递归究竟是如何发生的,取决于 JVM 编写者是如何实现的。这是一个描述如何在 OpenJDK 中完成的链接(包括 C++ 代码的链接): https ://wikis.oracle.com/display/HotSpotInternals/VirtualCalls

于 2013-08-06T15:18:29.267 回答