性能文章>【译】JVM内部是如何实现多态性的?(内部方法重载和重写)>

【译】JVM内部是如何实现多态性的?(内部方法重载和重写)转载

2周前
201315

在本文中,我们将看到JVM如何处理内部方法重载和重写,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
    }
}

我们可以用两种方式回答这个答案,逻辑方式和物理方式,让我们看看逻辑方式。

逻辑方式

从逻辑上讲,我们可以说,在编译阶段,调用方法从引用类型中考虑。但在执行时,方法将从引用持有的对象调用。

例如,在humanMammal.speak();行编译器上会说Mammal.speak()被调用是因为humanMammal是Mammal类型。在执行过程中,JVM知道humanMammal持有人类的对象,因此将调用Human.speak()。

好吧,在我们把它保持在概念层面之前,它非常简单。一旦我们怀疑JVM如何在内部处理这一切?或者JVM如何计算它应该调用哪种方法。

此外,我们知道重载方法不被称为多态方法,而是在编译时得到解决,这就是为什么有时方法重载也被称为编译时多态性或早期/静态绑定

但重写方法在运行时得到解决,因为编译器不知道,我们分配给引用的对象是否重写了该方法。

物理方式

在本节中,我们将尝试找到上述所有语句的物理证明,为了找到它们,我们将阅读程序的字节码,我们可以通过执行javap-verbose OverridingInternalExample来做到这一点。通过使用-verbose选项,我们将获得与我们的Java程序相同的描述性字节码。

上面的命令在两个部分显示了字节码

1.常量池:保存我们程序执行所需的几乎所有内容,例如方法引用(#Methodref)、类对象(#Class)、字符串文字(#String),请单击一个图像进行缩放。

java-method-area-or-constant-pool-or-method-table

2.程序的字节码:可执行的字节码说明,请单击一张图像进行缩放。

方法重载-重写-内部-字节代码

为什么方法过载被称为静态绑定

在上面提到的代码humanMammal.speak()编译器将说speak()是从Mammal调用的,但在执行时,它将从humanMammal持有的对象(Human类的对象)调用。

通过查看上述代码和图像,我们可以看到humanMammal.speak()、human.speak()和human.speak("印地语")的字节码完全不同,因为编译器可以根据类引用区分它们。

因此,在方法重载的情况下,编译器可以在编译时识别字节码指令和方法的地址,这就是为什么它也被称为静态绑定或编译时多态性

为什么方法重写被称为动态绑定

anyMammal.speak()和humanMammal.speak()的字节码是相同的(调用虚拟#4 //方法org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V),因为根据编译器,这两种方法都基于哺乳动物引用调用。

因此,现在问题来了,如果两个方法调用都有相同的字节码,那么JVM如何知道调用哪种方法?

好吧,答案隐藏在字节码本身中,它是调用虚拟指令,根据JVM规范调用虚拟调用对象的实例方法,在对象的(虚拟)类型上调度。这是Java编程语言中的正常方法调度。

JVM使用调用虚拟指令来调用相当于C++虚拟方法的Java。在C++中,如果我们想在另一个类中覆盖一个方法,我们需要将其声明为虚拟方法。但在Java中,默认情况下,所有方法都是虚拟的(最终和静态方法除外),因为我们可以覆盖子类中的每个方法。

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

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

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

Java虚拟机不为对象4强制任何特定的内部结构。

The bookmarks four states.

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

这意味着每个引用变量都有两个隐藏的指针。

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

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

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

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

每个类只有一个vtable,这意味着它是唯一的,对于类中与类对象相似的所有对象都是唯一的和相同的。

因此,Object类只有一个vtable,其中包含所有11种方法(如果我们不计算registerNatives)和对各自方法体的引用。

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

现在轮到Human类了,现在JVM将把所有条目从哺乳动物类的vtable复制到Human的vtable,并为重载版本的sput(字符串)添加一个新的条目。

JVM知道人类类重写了两种方法,一种是来自Object的toString(),另一个是来自Mammal的speck()。现在,而不是使用更新的引用为这些方法创建新条目。JVM将修改对之前同一索引上已经存在的方法的引用,并将保留相同的方法名称。

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

我希望现在会变得有点清楚,JVM如何混合常量池条目和vtable,以得出它将调用哪种方法。

你也可以在此Github仓库上找到完整的代码,请随时提供有价值的反馈。

原文作者:Naresh Joshi

点赞收藏
金色梦想

终身学习。

请先登录,查看1条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

没有二十年功力,写不出Thread.sleep(0)这一行“看似无用”的代码!

没有二十年功力,写不出Thread.sleep(0)这一行“看似无用”的代码!

JVM 常见线上问题:CPU100%、内存泄露的问题排查

JVM 常见线上问题:CPU100%、内存泄露的问题排查

【全网首发】微服务10:系统服务熔断、限流

【全网首发】微服务10:系统服务熔断、限流

【全网首发】MQ-消息堆积-JDK Bug导致线程阻塞案例分析

【全网首发】MQ-消息堆积-JDK Bug导致线程阻塞案例分析

FullGC没及时处理,差点造成P0事故

FullGC没及时处理,差点造成P0事故

【全网首发】一次想不到的 Bootstrap 类加载器带来的 Native 内存泄露分析

【全网首发】一次想不到的 Bootstrap 类加载器带来的 Native 内存泄露分析

5
1