在前面的文章中,有详细地介绍java字节码相关的知识,有兴趣的可以提前了解一下。
通过:7.Java字节码角度分析方法调用 ——提升硬实力7,我们已经知道了如下知识:
- invokespecial只能调用三类方法:<init>方法、private方法、super.method()。因为这三类方法的调用对象在编译时就可以确定。
- invokevirtual是一种动态分派的调用指令:也就是引用的类型并不能决定方法属于哪个类型。
接下来,我们将以字节码的视角来分析java多态原理,先来看一下java多态实现的示例:
// 从字节码角度来分析:多态原理
/**
* 演示多态原理,注意加上下面的 JVM参数,禁用指针压缩
* -XX:-UseCompressedOops -XX:-UseCompressedClassPointers
*/
public class T12_ByteAnalyseDuoTai {
// 此处,就会有多态应用。
public static void test(Animal animal) {
// 因为animal有可能是狗,也可能是猫,且不同对象eat方法实现不一样
// 后续将演示eat方法是哪个对象调用,从字节码角度对eat方法查找调用过程
animal.eat();
System.out.println(animal.toString());
}
public static void main(String[] args) throws IOException {
test(new Cat());
test(new Dog());
System.in.read();
}
}
abstract class Animal {
public abstract void eat();
@Override
public String toString() {
return "我是" + this.getClass().getSimpleName();
}
}
class Dog extends Animal {
@Override
public void eat() {
System.out.println("啃骨头");
}
}
class Cat extends Animal {
@Override
public void eat() {
System.out.println("吃鱼");
}
}
上述代码通过:javap -v T12_ByteAnalyseDuoTai.class进行反编译,得到如下字节码。得到:动态方法在字节码层面是通过invokevirtual 来调用的。
上述代码产生的多态应用,因为Animal具体实例有可能是狗,也可能是猫,且不同对象 eat() 方法实现不一样,接下来将演示 eat() 方法是哪个对象调用,从字节码角度对 eat() 方法查找调用过程
1、运行代码
- 停在System.in.read() 方法上,这时运行jps获取进程id
2、运行HSDB工具
- 进入JDK安装目录,执行:java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB
- 进入图形界面attach 进程id
3、HSDB - HotSpot Debugger -> Tools -> Find Object by Query
输入查询语句:select d from com.jvm.t07_bytecode.T12_ByteAnalyseDuoTai.Dog d,如下图
点击Dog 对象内存地址,就会得到Dog对象在内存的实际表示,如下图:
接下来,继续通过Dog对象内存地址继续查看Dog对象在内存的表现形式
我们复制0x000000001c794028,继续在【Tools】-> 【Inspector】查看对象在java内存的完整表示,如下:
接下来,继续查找多态方法。多态方法是存在于一张叫vtable虚方法表中,在Dog类对象在java内存的完整表示最后面。找到虚方法表,需要在当前地址 0x000000001c794028 加上 1B8 = 0x000000001c7941E0
接下来通过【Windows】 -> 【console】查找Dog类对应的虚方法表信息,指令如下:其中6是指虚方法表的长度
- mem 0x000000001c7941E0 6
得到了Dog类对应所有重写方法的入口地址,如下图:
上述说的Dog类所有重写的方法,具体对应哪些方法呢。我们知道Dog类继承自Animal,Animal继承自Object。接下来,分别从Dog、Animal、Object类进行查找。通过【Tools】 -> 【Class Browser】
说明:Dog类vtable虚方法表中调用的 eat() 方法是 Dog 类的 eat() 方法,不是 Cat 类的 eat() 方法,也不是 Animal 类的 eat() 方法。
接下来继续虚方法表vtable其他方法调用的是哪个方法,查找Dog类的父类Animal类, 如下图:
说明:Dog类虚方法表中 toString() 方法调用的是 Animal 的 toString() 方法,因为 Dog 类没有 toString() 方法,因父类 Animal 有 toString() 方法。
最后,再来看一下Object类,如下图:
总结:通过对象找到它的 Class 类,获取到它的虚方法表后,就能确定虚方法表中每个方法实际的方法入口地址。有的来自于自己(eat方法),有的来自于父类(toString方法)。将来,对象调用方法时,就能明确知道调用哪个方法了。最后提醒大家,虚方法表是在类的加载过程的链接阶段生成的,所以在链接阶段就已经确定了虚方法表每个方法的入口地址
小结
一句话总结:invokevirtual 指令调用的对象vtable中的方法。
多态方法调用,当执行 invokevirtual指令时,
- 先通过栈帧中的对象引用找到对象
- 分析对象头,找到对象的实际Class
- Class结构中有vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了
- 查表到得方法的具体地址
- 执行方法的字节码
多态方法调用,需要在执行过程中经过虚方法表多次查找,过程比较复杂。如果从细微的效率来说,它是不如static。
当然jvm底层也做很多虚方法表查找过程的优化,比如缓存、经常查找的方法放入缓存,这样查找较快。如果Animal只有Dog继承,没有Cat继承的话,jvm会将多态转换为单态,这样加快方法的寻址速度。
文章最后,给大家推荐一些受欢迎的技术博客链接:
- Hadoop相关技术博客链接
- Spark 核心技术链接
- JAVA相关的深度技术博客链接
- 超全干货--Flink思维导图,花了3周左右编写、校对
- 深入JAVA 的JVM核心原理解决线上各种故障【附案例】
- 请谈谈你对volatile的理解?--最近小李子与面试官的一场“硬核较量”
- 聊聊RPC通信,经常被问到的一道面试题。源码+笔记,包懂
欢迎扫描下方的二维码或 搜索 公众号“10点进修”,我们会有更多、且及时的资料推送给您,欢迎多多交流!