Java 的多态在 JVM 里原来是这样的
source link: http://mp.weixin.qq.com/s?__biz=MzI3MTEwODc5Ng%3D%3D&%3Bmid=2650861022&%3Bidx=1&%3Bsn=c0d77ad9acff85342ea7bb9da7ca190e
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
多态
面向对象的编程语言里,「 多态
」是一个至关重要的概念。我们常说,面向对象的本质,是 方法与数据的绑定
。那对于一个拥有继承关系的类之间,方法的绑定,是终是子类「重写」父类的方法,通过父类的引用指向子类的对象,实现运行时的多态。
说起来比较绕,我们先以仅次于Hello World 著名的 「动物 - 狗」代码来说明多态,然后再来分析在 JVM 层面,多态是怎样实现的。
package com.example.demo;
public class Demo {
public static void main(String[] args) {
Animal a = new Animal();
a.say();
Dog d = new Dog();
d.say();
Animal ad = new Dog();
ad.say();
}
}
class Animal {
public void say() {
System.out.println("Animal say");
}
public void play() {
System.out.println("play...");
}
}
class Dog extends Animal {
public void say() {
System.out.println("Dog say");
}
}
输出的内容对于习惯了面向对象的 Java 开发者来说都比较熟悉
Animal say
Dog say
Dog say
那虚拟机是怎样知道到底要调用 Animal 的 say 还是 Dog 的say呢?
咱们从字节码的层面来看一下。
0 new #2 <com/example/demo/Animal>
3 dup
4 invokespecial #3 <com/example/demo/Animal.<init>>
7 astore_1
8 aload_1
9 invokevirtual #4 <com/example/demo/Animal.say>
12 new #5 <com/example/demo/Dog>
15 dup
16 invokespecial #6 <com/example/demo/Dog.<init>>
19 astore_2
20 aload_2
21 invokevirtual #7 <com/example/demo/Dog.say>
24 new #5 <com/example/demo/Dog>
27 dup
28 invokespecial #6 <com/example/demo/Dog.<init>>
31 astore_3
32 aload_3
33 invokevirtual #4 <com/example/demo/Animal.say>
36 return
你发现没有,在字节码的第9行,和第33行,分别对应到 d.say() 和 ad.say() ,但指令内容其实是一样的。这就神奇了。
在这两个方法执行前,第8行和第32行,会有一个aload的操作,前面的文章里有介绍过( 看看 JVM 是怎样消化字节码指令的 ~~ ),是把这两个对象的引用 压到栈顶,给后面的操作用。这两个对象,一般也被称为方法的接收者(Receiver),如果熟悉 Golang等语言的朋友,对这个概念也不陌生。
从9行和第33行看,无论是方法调用的字节码指令还是参数,都指向了常量池的第4项。都是一样的,但最终结果并不相同。这里的重点在于 invokevirtual 这个指令的多态指行查找过程,即根据对象的
vtable
在运行时定位方法。
啥是 vtable?
前面的内容提到指令执行时从栈顶获取当前方法的「 接收者 」,通过invokerirtual 来执行这个接者者对应的方法。 注意这里的 virtual,和C++的虚方法类似。这个咱们不提,只说Java 的。
对象都有一个自己的「方法表」,这个表里除了自己的方法,还有从父类继承来的方法,甚至重写的父类的方法。所以,对应于 重写
与 重载
,体现在方法表里也有所区别。每个子类继承父类的时候,都将直接复制一份父类的方法表,而对于父类方法的重写,会直接更新方法表里相同顺序的这个方法。
而重载,本质上由于签名及参数的区别,是一个新的方法,在方法表里会是新增一个元素。
这里的这个 方法表 ,就是咱们说的 vtable(Virtual Method Table),表里的每个方法,对应的是它的实际执行入口地址。如果没有重写,那父类和子类的地址是一样的,都指向父类的实现。
如 果子类重写之后,子类方法表里的这个方法的地址就指向了自己实现的版本。
而我们上面字节码处观察到的,两个 invokevirtual 对应的常量池索引序号是一样的,这样实现对于变换实现类型时,查找方法表只需要换个对象,索引依旧相同。
观察
理解了方法表大概的原理,我们来解剖下,上咱们的JVM「显微镜」( Java虚拟机的显微镜 Serviceability Agent )。
为了便于 Attach 到 Java 进程,可以在代码里加下 latch 进行 awiat 阻塞,启动 SA 就能观察了。
选择 ClassBrowser
在 Class列表里就能找到咱们上面创建的对象。@ 符号后面是这个对象对应的内存地址。复制上Dog的地址,再从菜单里选择Inspector,
你看 _vtable_len: 7
这是告诉我们 vtable 长度是7,里面有7个方法。
实际上咱们在这个类里只重写了父类 Animal 的 say方法,其它的是从 Animal 继承来的 play方法,以及超类 Object 里的 5个方法,大概这个样子
JVM 在首次加载类的时候,会解析类内包含的方法,方法解析之后就会计算当前类 vtable的大小。
可能你会问,Object 类内不止5个方法,为什么只算5个呢?而且我们新增其它static、 final 这一类的方法呢?
这里 vtable 只计算非static final 的,全部计算完就得出了vtable_len这个值。
每个 Java 的 Class 在 JVM 内部都会有一个自己的instanceKlass, vtable就分配在这个的最后。
整个instanceKlass的大小,在64位系统里大小是 0x1b8 ,记住它,后面用的着。 所以咱们上面看到了Dog 类的内存地址,继续找就能看到他其它方法对应的内存地址。
在Windows -> console 里执行这个:
mem 0x7C0060DD0 7
这个值怎么来的呢?是从对象的内存地址开始,加上 instanceKlass的大小。
0x7C0060DD0 = 0x00000007c0060c18 + 0x1b8
由于我们有7个方法,所以顺序查找7个地址。
所以你应该也发现了,Java 里对应这种重写的方法,是在类加载的时候,才能知道具体对应的是哪个方法,因此也被称为动态绑定或者迟绑定。
总结起来,这里的 vtable,相当于你的工具清单,有什么能力都做了罗列,像钢铁侠的各项技能,每个功能指向具体的超能力,在我们代码里可以把它理解成一个数组,数组的每个元素指向一个方法地址。
感兴趣的话,你加个static 的方法自己找找,看看在不在这里面呢?毕竟static方法执行不是有 invokestatic 指令嘛。
看点别的
你写下的try-catch-finally,在JVM看来不过是...
Java虚拟机的显微镜 Serviceability Agent
更多常见问题,请关注公众号,在菜单「 常见问题 」中查看 ,也欢迎加我微信,一起交流。
源码|实战|成长|职场
这里是「 Tomcat那些事儿 」
请留下你的足迹
我们一起「终身成长」
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK