本文的翻译是专门为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存储库中找到示例代码。