本文的翻译是专门为Java Developer课程的学生准备的。
在我之前的文章
《关于方法重载与方法重载的一切》中 ,我们研究了方法重载和重载的规则和差异。 在本文中,我们将看到如何在JVM中处理方法重载和重写。
例如,以上一篇文章中的类为例:父级
Mammal
(哺乳动物)和子级
Human
(人类)。
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"); }
我们可以从两个方面看待多态性问题:从“逻辑”和“物理”两个方面。 让我们首先看问题的逻辑方面。
逻辑观点
从逻辑角度来看,在编译阶段,被调用的方法被认为与引用的类型有关。 但是在运行时,将调用所引用对象的方法。
例如,在一行
humanMammal.speak();
编译器认为将调用
Mammal.speak()
,因为
humanMammal
声明为
Mammal
。 但是在运行时,JVM会知道
humanMammal
包含一个
Human
对象,并将实际调用
Human.speak()
方法。
只要我们保持概念上的水平,这一切都非常简单。 但是,JVM如何在内部处理所有这些呢? JVM如何计算应调用哪个方法?
我们还知道重载方法不称为多态方法,而是在编译时解析的。 尽管有时方法重载称为
编译时多态性或早期/静态绑定 。
重写的方法(重写)在运行时解析,因为编译器不知道分配给链接的对象中是否存在重写的方法。
物理角度
在本节中,我们将尝试为上述所有陈述寻找“物理”证据。 为此,请查看通过运行
javap -verbose OverridingInternalExample
可以获得的字节码。
-verbose
参数将使我们能够获得与我们的Java程序相对应的更直观的字节码。
上面的命令将显示字节码的两个部分。
1.常量池 。 它包含了运行程序所需的几乎所有内容。 例如,方法引用(
#Methodref
),类(
#Class
),字符串文字(
#String
)。
2.程序的字节码。 可执行字节码指令。

为什么方法重载称为静态绑定
在上面的示例中,编译器认为将从
Mammal
类中调用
humanMammal.speak()
方法,尽管在运行时将从
humanMammal
引用的对象中
humanMammal
它-这将是
Human
类的对象。
查看我们的代码和
javap
的结果,我们看到使用了不同的字节码来调用方法
humanMammal.speak()
,
human.speak()
和
human.speak("Hindi")
,因为编译器可以根据类引用来区分它们。
因此,在方法过载的情况下,编译器能够在编译时识别字节码指令和方法地址。 这就是为什么将其称为
静态链接或编译时多态性的原因。为什么方法重写称为动态绑定
要调用
anyMammal.speak()
和
humanMammal.speak()
方法,字节码是相同的,因为从编译器的角度来看,这两种方法都是针对
Mammal
类调用的:
invokevirtual #4 // Method org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V
所以现在的问题是,如果两个调用都具有相同的字节码,那么JVM如何知道要调用哪个方法?
答案隐藏在字节码本身和
invokevirtual
指令中。 根据JVM规范
(译者注: 参考JVM规范2.11.8 ) :
invokevirtual指令通过分派对象的(虚拟)类型来调用实例方法。 这是Java编程语言中方法的常规分配。
JVM使用
invokevirtual
在Java方法中调用等效于C ++虚拟方法的方法。 在C ++中,要覆盖另一个类中的方法,必须将该方法声明为virtual。 但是在Java中,默认情况下,所有方法都是虚拟的(最终和静态方法除外),因此在子类中,我们可以覆盖任何方法。
invokevirtual
指令采用指向要调用的方法的指针(#4是常量池中的索引)。
invokevirtual #4
但是参考文献#4进一步引用了另一个方法和类。
#4 = Methodref #2.#27
所有这些链接一起使用以获得对方法和所需方法所在的类的引用。 JVM规范中也提到了这一点(
译者注: 对JVM规范2.7的引用 ):
Java虚拟机不需要任何特定的对象内部结构。
在Oracle的一些Java虚拟机实现中,对类实例的引用是对处理程序的引用,该处理程序本身由一对链接组成:一个指向对象方法表,一个指向表示对象类型的Class对象的指针,另一个指向区域包含对象数据的堆上的数据。
这意味着每个引用变量都包含两个隐藏的指针:
- 指向包含该对象的方法的表的指针和一个指向
Class
对象的指针,例如[speak(), speak(String) Class object]
- 指向堆上分配给对象数据(例如对象字段值)的内存的指针。
但是,问题再次出现:如何在此方面进行
invokevirtual
? 不幸的是,没有人可以回答这个问题,因为这完全取决于JVM的实现,并且因JVM而异。
根据以上推理,我们可以得出结论,对一个对象的引用间接包含指向该表的链接/指针,该表包含对该对象方法的所有引用。 Java从C ++借用了这个概念。 该表以各种名称众所周知,例如
虚拟方法表(VMT),虚拟功能表(vftable),虚拟表(vtable),调度表 。
我们不确定vtable如何用Java实现,因为它取决于特定的JVM。 但是我们可以预期该策略将与C ++中的策略大致相同,其中vtable是一个类似数组的结构,其中包含方法名称及其引用。 每当JVM尝试执行虚拟方法时,它都会在vtable中请求其地址。
对于每个类,只有一个vtable,这意味着该表是唯一的,并且对于该类的所有对象都是相同的,类似于Class对象。 类对象在文章
为什么外部Java类不能是静态的以及
为什么Java是纯面向对象的语言或为什么不是这样的文章中有更详细的讨论。
因此,
Object
类只有一个vtable,其中包含所有11种方法(如果不考虑
registerNatives
话)以及与它们的实现相对应的链接。

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

然后,进入
Human
类的类,并且JVM将所有条目从
Mammal
类的vtable复制到
Human
类的vtable,并为
speak(String)
的重载版本添加新条目。
JVM知道
Human
类重写了两个方法:
Object
toString()
和
Mammal
speak()
。 现在,对于这些方法,JVM不再使用更新的链接来创建新记录,而是将链接更改为以前存在它们的相同索引中现有方法的链接,并保留相同的方法名称。

invokevirtual
指令使JVM处理对方法4的引用中的值,而不是作为地址,而是作为要在vtable中查找当前对象的方法的名称。
我希望现在可以更清楚地了解JVM如何使用常量池和虚拟方法表来确定要调用的方法。
您可以在
Github存储库中找到示例代码。