【译】JVM内部是如何实现多态性的?(内部方法重载和重写)转载
在本文中,我们将看到JVM如何处理内部方法重载和重写,JVM如何识别应该调用哪种方法。
让我们用之前博客中雌性哺乳动物和儿童人类为例,来举个例子。
我们可以用两种方式回答这个答案,逻辑方式和物理方式,让我们看看逻辑方式。
逻辑方式
从逻辑上讲,我们可以说,在编译阶段,调用方法从引用类型中考虑。但在执行时,方法将从引用持有的对象调用。
例如,在humanMammal.speak();行编译器上会说Mammal.speak()被调用是因为humanMammal是Mammal类型。在执行过程中,JVM知道humanMammal持有人类的对象,因此将调用Human.speak()。
好吧,在我们把它保持在概念层面之前,它非常简单。一旦我们怀疑JVM如何在内部处理这一切?或者JVM如何计算它应该调用哪种方法。
此外,我们知道重载方法不被称为多态方法,而是在编译时得到解决,这就是为什么有时方法重载也被称为编译时多态性或早期/静态绑定。
但重写方法在运行时得到解决,因为编译器不知道,我们分配给引用的对象是否重写了该方法。
物理方式
在本节中,我们将尝试找到上述所有语句的物理证明,为了找到它们,我们将阅读程序的字节码,我们可以通过执行javap-verbose OverridingInternalExample来做到这一点。通过使用-verbose选项,我们将获得与我们的Java程序相同的描述性字节码。
上面的命令在两个部分显示了字节码
1.常量池:保存我们程序执行所需的几乎所有内容,例如方法引用(#Methodref)、类对象(#Class)、字符串文字(#String),请单击一个图像进行缩放。
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是常量池的索引)
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强制任何特定的内部结构。
The bookmarks four states.
在Oracle对Java虚拟机的一些实现中,对类实例的引用是对句柄的指针,该句柄本身是一对指针:一个指向包含对象方法的表,指向表示对象类型的Class对象的指针,另一个指向从堆中为对象数据分配的内存。
这意味着每个引用变量都有两个隐藏的指针。
- 指向再次保存对象方法的表的指针和指向类对象的指针。例如[speak(), speak(String) Class object]。
- 指向堆上为该对象数据分配的内存的指针,例如实例变量的值。
问题又来了,如何在内部调用虚拟做到这一点?好吧,没有人能回答这个问题,因为它取决于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