从下面的例子开始:
package dog.test;
//被调用的父类
public class Dog {
public void bark() {
System.out.println("Woof!");
}
}
package dog.test;
public class ShepherdDog extends Dog {
@Override
public void bark() {
System.out.println("Woof!Woof!Woof!");
}
}
import dog.test.*;
public class AutoCall{
public static void main(String[] args){
//多态
Dog dog = new ShepherdDog();
//打印结果: Woof!Woof!Woof!
dog.bark();
}
}
了解 Java 语法的都知道这段程序会输出:
Woof!Woof!Woof!
原因是 Java 中的多态,bark() 方法已经被子类覆写(override)过,所以程序调用的是子类的 bark() 。
那 JVM 是怎么知道 dog.bark() 调用的是子类的方法而非父类的呢?
首先先介绍一下 JVM 管理的一个非常重要的数据结构——方法表。
在JVM加载类文件时,会在运行时数据区里存放一些与该类有关的信息,运行时数据区由方法区、堆区、栈区、程序计数器等组成。方法表就存放在方法区中。
方法表以数组的形式记录了当前类和其所有超类的可见方法字节码在内存中的直接地址。
方法表有两个特点:
- 子类方法表继承父类的方法;
- 相同的方法(相同方法签名:方法名和参数列表)在所有类的方法表中的索引相同。
多态调用的字节码指令代码
0 new dog.test.ShepherdDog [13] //在堆中开辟一个 ShepherdDog 对象的内存空间,并将对象引用压入操作数栈
3 dup
4 invokespecial #7 [15] // 调用初始化方法来初始化堆中的 ShepherdDog 对象
7 astore_1 //弹出操作数栈的 ShepherdDog 对象引用压入局部变量1中
8 aload_1 //取出局部变量1中的对象引用压入操作数栈
9 invokevirtual #15 //调用 bark() 方法
12 return
-
invokevirtual 指令中的#15指的是 AutoCall 类的常量池中第15个常量表的索引项。这个常量表(CONSTATN_Methodref_info ) 记录的是方法 bark() 信息的符号引用(包括 bark() 在的类名,方法名和返回类型)。 JVM 会首先根据这个符号引用找到调用方法 bark 的类的全限定名: dog.test.Dog 。这是因为调用方法 bark() 的类的对象 dog 声明为 Dog 类型。
-
在 Dog 类型的方法表中查找方法 bark() ,如果找到,则将方法 bark 在方法表中的索引项11(如上图)记录到 AutoCall 类的常量池中第15个常量表中(常量池解析 )。这里有一点要注意:如果 Dog 类型方法表中没有方法 bark ,那么即使 ShepherdDog 类型中方法表有,编译的时候也通过不了。因为调用方法 bark() 的类的对象 dog 的声明为 Dog 类型。
-
在调用 invokevirtual 指令前有一个 aload_1 指令,它会将开始创建在堆中的 ShepherdDog 对象的引用压入操作数栈。然后invokevirtual指令会根据这个 ShepherdDog 对象的引用首先找到堆中的 ShepherdDog 对象,然后进一步找到 ShepherdDog 对象所属类型的方法表。
-
这是通过第(2)步中解析完成的#15常量表中的方法表的索引项11,可以定位到 ShepherdDog 类型方法表中的方法 bark(),然后通过直接地址找到该方法字节码所在的内存空间。
那么我们发现,仅仅根据对象 dog 和声明类型 Dog 并不能确定调用方法 dark() 的位置,必须根据 dog 在堆中实际创建的对象 ShepherdDog 来确定 dark() 所在的位置。
这种在程序运行中,通过动态创建对象来定位方法的方式,就叫做动态绑定机制。
网友评论