首页
社区
课程
招聘
从jvm虚拟机角度看Java多态 ->(重写override)的实现原理
发表于: 2018-3-23 19:01 9140

从jvm虚拟机角度看Java多态 ->(重写override)的实现原理

2018-3-23 19:01
9140

工具与环境:

Windows 7 x64企业版

Cygwin x64

jdk1.8.0_162

openjdk-8u40-src-b25-10_feb_2015

Vs2010 professional

个人博客:http://www.cnblogs.com/2014asm/

1.多态的概念:

JAVA类被jvm加载运行时根据调用该方法的对像实例的类型来决定选择调用哪个方法则被称为运行时多态。也叫动态绑定:是指在执行期间判断所引用对象实例的实际类型,根据其实际的类型调用其相应的方法。

2.多态的优点:

a.可替换性: 多态对已存在代码具有可替换性。

b.可扩充性: 多态对代码具有可扩充性。
c.灵活性: 它在应用中体现了灵活多样的操作,提高了使用效率。
d.简化性: 多态简化对应用软件的代码编写和修改过程,尤其在处理大量对象的运算和操作时,这个特点尤为突出和重要。

3.示例代码(以下分析基于该代码):

编译: javac Animal.java生成.class文件。

运行后如下图:

4.上面示例程序中定义了类 Animal ,同时定义了 2 个子类 Dog 和 Cat,这 2 个子类都重写了基类中的 say()方法 。 在 main()函数中,将 animal 实例引用分别指向 Dog 和 Cat 的实例, 并分别调用 run(Animal)方法。 在本示例中,当在 Animal.run(Animal)方法中执行 animal.say()时, 因为

在编译期并不知道 animal 这个引用到底指向哪个实例对象,所以编译期无法进行绑定,必须等到运行期才能确切知道最终调用哪个子类的 say()方法,这便是动态绑定,也即晚绑定,这是 Java语言以及绝大多数面向对象语言的动态机制最直接的体现。

1.  在分析JVM多态的实现原理之前,我们先一起看看 C++中虚方法表的实现机制,这两者有很紧密的联系,有助于我们理解JVM中的多态机制。

2.  示例代码:

这个 C++示例很简单,类中包含一个 int 类型的变量和一个 run()方法,在 main函数中定义一个Cpuls对像。通过vs 调试时看内存情况,没有虚函数时对象的内存表现如下图:

由于 CPLUS 类中仅包含 l 个 int 类型的变量 ,因此观察结果中的 cplus 实例内存地址,只有变量 x 。

现在将 C++类中的 run方法修改一下,变成虚方法,在观察对象的内存表现:

注意看,现在的值变了,cplus 实例首地址不是其变量x了,而是一个vfable,这就是虚表,并且vfable中存放加了virtual关键字的虚函数func函数的地址,这是因为当 C++类中出现虚方法时,表示该方法拥有多态性,此时会根据类型指针所指向的实际对象而在运行期调用不同的方法。

C++为了实现多态,就在 C++类实例对象中嵌入虚函数表vfable ,通过虚函数表来实现运行期的方法分派 。 C++中所谓虚函数表,其实就是一个普通的表,表中存储的是方法指针, 方法指针会指向目标方法的内存地址,所以虚函数表就是一堆指针的集合而已。

详细的可以看这位大牛的分析https://bbs.pediy.com/thread-221160.htm

1.  Java中的多态在语义上与上面分析C++的原理是相同的,Java在JVM中的多态机制并没有跳出这个圈也采用了 vftable 来实现动态绑定。

JVM 的 vftable 机制与 C++的 vftable机制之间的不同点在于, C++的 vftable 在编译期间便由编译器完成分析和模型构建,而 JVM 的 vftable 则在 JVM 运行期类被加载时进行动态构建。下面通过hotspot源码来分析JVM中函数重写机制。

2.  当我们通过java 执行class文件时,JVM 会在第一次加载类时调用classFileParser.cpp::parseClassFile()函数对 Java class 文件字节码进行解析,在parseClassFile()函数中会调用parse_methods()函数解析class文件类中的方法,parse_methods()函数执行完之后 ,会继续调用 klassVtable::compute_vtable_size_and_num_mirandas()函数,计算当前类的vtable大小,下面看看该方法实现的主要逻辑:

判断是否有虚函数,如果有就将个数增加, src\share\vm\oops\klassVtable.cpp

上面这段代码计算 vftable 个数的思路主要分为两步 :

a:获取父类 vftable 的个数,并将当前类的 vftable 的个数设置为父类 vftable 的个数。

b:循环遍历当前 Java 类的每一个方法 ,调用 needs_new_vtable_entry()函数进行判断,如果判断的结果是 true ,则将 vftable 的个数增 1 。

3.  现在看needs_new_vtable_entry()函数是如何判断虚函数的,判断条件是什么?

上面代码主要判断Java 类在运行期进行动态绑定的方法,一定会被声明为 public 或者 protected 的,并且没有 static 和 final 修饰,且 Java 类上也没有 final 修饰 。

4.  当class文件被分析完成后就要创建一个内存中的instanceKlass对象来存放class信息,这时就要用到上面分析的虚表个数了vtable_size。该变量值将在创建类所对应的instanceKlass对象时被保存到该对象中的一vtable_Ien 字段中。

5.  当class分析并将相关的信息存放在instanceKlass实例对像中后就准备要执行函数了, 在分析重写之前我们来看看vtable在什么地方。

每一个 Java 类在 JVM 内部都有一个对应的instanceKlass, vtable 就被分配在这个 oop 内存区域的后面。

instanceKlass大小在 windows64系统的大小为0x1b8如下,后面用hsdb查看vtable时会用到。

6.  方法的重写主要在该函数中:

klassVtable::initialize_vtable(bool checkconstraints, TRAPS)函数主要逻辑:

以上代码逻辑主要是调用update_inherited_vtable函数判断子类中是否有与父类中方法名签名完全相同的方法,若该方法是对父类方法的重写,就调用klassVtable::put_method_at(Method* m, int index)函数进行重写操作,更新父类 vtable 表中指向父类被重写的方法的指针,使其指向子类中该方法的内存地址。 若该方法并不是对父类方法的重写,则会调用klassVtable::put_method_at(Method* m, int index)函数向该 Java 类的 vtable 中插入一个新的指针元素,使其指向该方法的内存地址,增加一个新的虚函数地址。

当Hotspot在运行期加载类Animal时,其 vtable 中将会有一个指针元素指向其say方法在Hotspot内部的内存首地址,当 Hotspot 加载类 Dog 时, 首先类 Dog 完全继承其父类 Animal 的 vtable,因此类 Dog 便也有一个 vtable ,并且 vtable 里有一个指针指向类 Animal 的 say方法的内存地址 。

Hotspot 遍历类 Dog 的所有方法,并发现say方法是 public 的,并且没有被 static 、 final 修饰,于是 HotSpot 去搜索其父类中名称相同、签名也相同的方法,结果发现父类中存在一个完全一样的方法,于是 HotSpot就会将类 Dog 的 vtable 中原本指向类 Animal 的 say方法的内存地址的指针值修改成指向类Dog 自己的say方法所在的内存地址 。

1.  下面我们将通过hsdb来验证前面的分析。如前面所述,Java 类在 JVM 内部所对应的类型是 instanceKlass,根据上面一节的分析,我们知道vtable 便分配在 instanceKlass 对象实例的内存末尾 。 instanceKlass 对象实例在X64平台上内存中所占内存大小是 Oxlb8 字节(32位平台上 sizeof(InstanceKlass)=0x00000108),换算成十进制是 440。 根据这个特点,可以使用 HSDB获取到 Java 类所对应的 instanceKlass 在内存中的首地址,然后加上 Oxlb8 ,就得到 vtable 的内存地址 ,如此便可以查看这个内存位置上的 vtable 成员数据 。

还是用Animal文件做示例,类 Animal 中仅包含 1 个 Java 方法 ,因此类 Animal 的 vtable长度一共是 6 ,另外 5 个是超类 java.lang.Object 中的5个方法。使用JDB 调试(jdb -XX:-UseCompressedOops Animal),并运行至断点处使程序暂停(stop in Animal.main)->(run),jps查看ID,然后使用 HSDB 连接上测试程序(java -classpath "%JAVA_HOME%/lib/sa-jdi.jar" sun.jvm.hotspot.HSDB),打开 HSDB 的 Tools->Class Browser 功能,就能看到类 Animal 在 JVM 内部所对应的 instanceKlass 对象实例的内存地址,如图所示 。

由上图可知,类 Animal 在JVM内部所对应的 instanceKlass的内存首地址是 0x00000000320004a8 ,上一节分析知道vtable 被分配在 instanceKlass的末尾位置,因此 vtable 的内存首地址是 :

0x00000000320004a8 + Oxlb8 = Ox0000000032000660

这里的 Oxlb8 是 instanceKlass 对象实例所占的内存空间大小 。 得到 vtable 内存地址后,便可以使用 HSDB 的 mem 工具来查看这个地址处的内存数据。单击 HSDB 工具栏上的 Windows->Console 按钮,打开 HSDB 的终端控制台,按回车键,然后输入“ mem Ox32000660 6”命令,就可以查看从 vtable 内存首地址开始的连续 6 个双字内容,如下所示:



在 64 位平台上, 一个指针占 8 字节 ,而 vtable 里的每一个成员元素都是一个指针,因此这里 mem 所输出 的 6 行 ,正好是类 Animal 的 vtable 里的 6 个方法指针,每一个指针指向 l 个方法在内存中的位置。 类 A 的 vtable 总个数是 6 ,其中前面 5 个是基类 java.lang.Object 中的 5 个方法的指针 。上面 mem 命令所输出的第 6 行的指针, 一定就是指向类 Animal 自己的say方法的内存地址 。 使用HSDB 查看类 A 的方法的内存地址,如图中所示,地址刚好对应得上。其它的类也可以用同样的方式分析。

前面对jvm 的vtable 进行了研究和验证,再总结下特点:


[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 2018-3-23 19:22 被我是小三编辑 ,原因:
上传的附件:
收藏
免费 4
支持
分享
最新回复 (7)
雪    币: 23080
活跃值: (3432)
能力值: (RANK:648 )
在线值:
发帖
回帖
粉丝
2
感谢分享,欢迎楼主来分享更多该系列的知识
2018-3-24 14:21
0
雪    币: 8224
活跃值: (1296)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
感谢分享
还可以这么深入
2018-3-25 09:52
0
雪    币: 683
活跃值: (622)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
感谢分享
2018-3-25 13:55
0
雪    币: 6818
活跃值: (153)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
2018-3-25 23:22
0
雪    币: 7
活跃值: (84)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
牛人一个
2018-10-11 20:38
0
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
7
Animal animal = new Dog(); 和 Dog dog = new Dog(); 的区别是什么
2021-8-1 15:19
0
雪    币: 576
活跃值: (2035)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
感谢分享 mark
2021-8-1 18:57
0
游客
登录 | 注册 方可回帖
返回
//