OOP 多态机制在 JVM 中的实现
Posted ImportNew
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了OOP 多态机制在 JVM 中的实现相关的知识,希望对你有一定的参考价值。
(给ImportNew加星标,提高Java技能)
编译:ImportNew/覃佑桦
www.coderbuzz.com/2019/11/21/how-does-jvm-handle-polymorphism-internally/
本文将介绍面向对象编程多态机制在JVM中的内部实现。
本文将讨论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");
}
// 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";
}
}
// 下面的代码包含输出与方法调用字节码
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();这行代码,由于humanMammal的类型是Mammal,编译器会调用Mammal.speak()。在执行过程中,JVM知道humanMammal是一个Human对象,因此会调用Human.speak()。
目前为止只是从概念上理解,很简单对吧。当试图理解JVM如何在内部实现这些功能,以及如何计算应该调用哪个方法,就没那么简单了。
此外,我们知道方法重载是在编译时决定的,不能称作多态。这就是为什么有时候方法重载也称为编译时多态、早期绑定或静态绑定。
而方法覆写会在运行时解决,因为编译器不知道调用的对象是否覆写了对应的方法。
物理实现
本节会通过阅读字节码查找上面分析对应的物理实现,执行javap -verbose OverridingInternalExample。使用-verbose选项,会得到Java程序对应的描述性字节码。
上面命令得到的字节码包含两部分:
1.常量池:包含了执行程序所需的几乎所有内容,比如方法引用(#Methodref)、类对象(#Class)、字符串(#String)。
2.程序字节码:可执行的字节码指令。
为什么方法重载也称为静态绑定
前面提到的humanMammal.speak(),编译器会从Mammal类中调用speak()。但实际执行中,将从humanMammal对应的Human对象中调用。
从上面的代码和图中可以看到,由于编译器会根据类的不同进区别处理,因此humanMammal.speak()、human.speak()和human.speak("Hindi")的字节码完全不同。
为什么方法覆写也称为动态绑定
anyMammal.speak()和humanMammal.speak()生成的字节码相同(invokevirtual #4 // Method org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V)。站在编译器的角度看,这两个调用的方法都来自Mammal对象。
现在的问题是,如果两个方法具有相同的字节码,那么JVM如何知道要调用哪一个?
答案就隐藏在字节码中。根据JVM规范,invokevirtual会调用对象的实例方法,并根据对象的(virtual)类型分派调用。这是Java编程语言中普通方法的分派。
JVM使用invokevirtual指令调用Java方法,与C++虚方法类似。在C++中,要覆写另一个类中某个方法,需要将其声明为虚方法。在Java中,所有方法默认都是虚方法(final和static方法除外)。我们可以在子类中覆写父类的每个方法。
invokevirtual操作接受一个指针作为参数,指向方法引用(#4是常量池中的索引)。
invokevirtual #4 // Method org/programming/mitra/exercises //OverridingInternalExample$Mammal.speak:()V
方法引用#4指向的方法名和Class。
#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规范中也提到了这一点。
对于#4这样的对象,Java虚拟机不要求对象具备任何特定的内部结构。
规范中还指出:
在Oracle的一些Java虚拟机实现中,对类实例的引用是一个指向句柄的指针,句柄本身也是一对指针:一个指向包含了对象方法的table和表示对象类型的Class对象指针,另一个指向为对象数据分配的堆内存。
这意味着每个对象引用都包含两个隐藏的指针。
table包含了对象方法以及指向Class对象的指针,例如[speak()、speak(String)、Class对象]。
堆内存中包含了对象数据,例如实例变量值。
那么问题又来了,JVM如何在内部调用virtualual?嗯,这个问题的答案根据JVM的具体实现各有不同。
从上面的内容可以得出结论,一个对象引用间接持有了table指针和Class指针。table中保存了该对象的所有方法引用。Java从C++借用了这个概念,有很多名字,例如virtual method table (_VMT_)、virtual function table (vftable)、virtual table (vtable)、dispatch table。
每个类只有一个vtable,这意味着它是唯一的,而且所有对象的vtable都与Class对象相同。我在“为什么Java外部类不能是静态的”和“Java为什么是或者不是纯粹的面向对象编程语言”文章中介绍了更多Class对象相关内容。
因此,Object类只有一个vtable,其中包含了所有11个方法(不计算registerNatives)以及对各自方法体的引用。
当JVM把Mammal类加载到内存中时,会为它创建一个Class对象及一个vtable。由于Mammal不覆写Object中的任何方法,因此vtable包含了Object类vtable中的所有方法(方法引用相同),同时添加了新的speak方法。
现在轮到Human类,JVM会把Mammal类中所有条目拷贝到Human类的vtable中,并为重载过的speak(String)方法新增条目。
JVM现在知道Human类已经覆写了两个方法,一个是Object的toString(),另一个是Mammal的speck()。现在,JVM不用为这些方法创建新条目,找到已经存在的方法索引更新引用即可,方法名不作修改。
invokevirtual发生时,JVM会把#4存储的值作为方法引用在当前对象的vtable中查找对应方法。
希望现在您已经开始理解JVM是怎样结合常量池和vtable信息决定调用哪个方法。
在Github仓库可以找到完整源代码,欢迎随时反馈提出宝贵的意见。
https://github.com/njnareshjoshi/exercises/blob/master/src/org/programming/mitra/exercises/OverridingInternalExample.java
推荐阅读
(点击标题可跳转阅读)
看完本文有收获?请转发分享给更多人
关注「ImportNew」,提升Java技能
好文章,我在看❤️
以上是关于OOP 多态机制在 JVM 中的实现的主要内容,如果未能解决你的问题,请参考以下文章