java中实现多态的机制6_OOP 多态机制在 JVM 中的实现

933e8813637217d3adf65074d851e71d.png

本文将介绍面向对象编程多态机制在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)。

f7a43ad6c58cd90e3da2930a48851037.png

2.程序字节码:可执行的字节码指令。

0934baaf763dbef0d23ccdd70a75251b.png

为什么方法重载也称为静态绑定

前面提到的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在Java中是如何实现的,因为这与具体的JVM相关,但是我们可以预期它将遵循与C++相同的策略。其中vtable是一种类似数组的结构,其中包含方法的名字及方法引用在数组中的索引。当JVM尝试执行虚拟方法时,总会向vtable请求方法的地址。

每个类只有一个vtable,这意味着它是唯一的,而且所有对象的vtable都与Class对象相同。我在“为什么Java外部类不能是静态的”和“Java为什么是或者不是纯粹的面向对象编程语言”文章中介绍了更多Class对象相关内容。

因此,Object类只有一个vtable,其中包含了所有11个方法(不计算registerNatives)以及对各自方法体的引用。

0864f04f70781f4f6925f19609cc1a35.png

当JVM把Mammal类加载到内存中时,会为它创建一个Class对象及一个vtable。由于Mammal不覆写Object中的任何方法,因此vtable包含了Object类vtable中的所有方法(方法引用相同),同时添加了新的speak方法。

51fd598553fb88a1ed51279a85a1ea3a.png

现在轮到Human类,JVM会把Mammal类中所有条目拷贝到Human类的vtable中,并为重载过的speak(String)方法新增条目。

JVM现在知道Human类已经覆写了两个方法,一个是Object的toString(),另一个是Mammal的speck()。现在,JVM不用为这些方法创建新条目,找到已经存在的方法索引更新引用即可,方法名不作修改。

edec0275e9793b5c6eff921e06e0a662.png

invokevirtual发生时,JVM会把#4存储的值作为方法引用在当前对象的vtable中查找对应方法。

希望现在您已经开始理解JVM是怎样结合常量池和vtable信息决定调用哪个方法。

在Github仓库可以找到完整源代码,欢迎随时反馈提出宝贵的意见。

https://github.com/njnareshjoshi/exercises/blob/master/src/org/programming/mitra/exercises/OverridingInternalExample.java

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值