动态连接及分派深入详解
- 为什么要将动态连接和分派放在一起讲?
- 动态连接概括定义
- 静态解析
- 分派
- 静态分派
- 动态分派
为什么要将动态连接和分派放在一起讲?
大家看完后面的内容这个问题迎刃而解了。
动态连接概括定义
每个栈帧都保存了一个可以指向当前方法所在类的运行时常量池, 目的是当方法中需要调用其它方法的时候能够从运行时常量池中找到对应的符号引用, 然后将符号引用转换为直接引用然后就能直接调用对应的方法这就是动态链接,不是所有的方法调用都需要进行动态链接的
有一部分的符号引用会在类加载的解析阶段将符号引用转换为直接引用,这部分操作称之为静态解析。
静态解析
类加载的解析阶段会将部分的符号引用解析为直接引用,这部分的符号引用指的是编译期间就能确定调用的版本,主要包括2大类
- invokestatic: 调用静态方法
- invokespecial: 调用实列构造器
<init>
私有方法,私有方法,父类方法
因为这2类不允许被重写修改, 符合"编译器可知,运行期不可变"的准则,把这类方法称为非虚方法
除去静态解析能在类加载的解析阶段将符号引用解析为直接引用,剩下的符号引用就要在运行期间进行解析。
分派
在运行期间,或者静态解析的时候,确定调用方法的时候方法就可能存在重载,重写等情况这里的分派将揭开"重载"和"重写"的实现原理以及他们的选用规则
静态分派
这里主要揭开了重载的实现规则
public class StaticDispatch {
public void sayHello(Human human) {
System.out.println("human hello world!");
}
public void sayHello(Man human) {
System.out.println("Man hello world!");
}
public void sayHello(Woman human) {
System.out.println("Woman hello world!");
}
public static void main(String[] args) {
StaticDispatch dispatch = new StaticDispatch();
Human man = new Man();
Human woman = new Woman();
dispatch.sayHello(man);
dispatch.sayHello(woman);
}
}
abstract class Human {
}
class Man extends Human {
}
class Woman extends Human {
}
输出:
human hello world!
human hello world!
为什么会输出这2个结果呢?主要是因为在编译期间Human的类型是确定的我们称之为静态类型,Man是只有在运行期间new Man()这个动作发生了后才知道它的具体类型我们称为实际类型,而重载是根据静态类型确定调用过程的所以都会去调用sayHello(Human human)。
这里要强调一点的是分派是确定调用方法的过程,在类的解析阶段,静态方法也存在重载也可以使用静态分派进行确定,在动态连接的确定方法调用版本的时候,也存在用分派确定调用,所以解析和分派不是分开进行的而是相互协作的。
动态分派
这里主要揭开了重写的实现规则
public class DynamicDispatch {
static abstract class Human {
protected abstract void sayHello();
}
static class Man extends Human {
@Override
protected void sayHello() {
System.out.println("Man say hello !");
}
}
static class Woman extends Human {
@Override
protected void sayHello() {
System.out.println("Woman say hello !");
}
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
man.sayHello();
woman.sayHello();
}
}
输出:
Man say hello !
Woman say hello !
相信大家一眼都能看出正确的结果,那么虚拟机是如何知道调用的哪个方法呢?下面来看看javap输出的字节码
public class test.jvm.DynamicDispatch
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #8.#30 // java/lang/Object."<init>":()V
#2 = Class #31 // test/jvm/DynamicDispatch$Man
#3 = Methodref #2.#30 // test/jvm/DynamicDispatch$Man."<init>":()V
#4 = Class #32 // test/jvm/DynamicDispatch$Woman
#5 = Methodref #4.#30 // test/jvm/DynamicDispatch$Woman."<init>":()V
#6 = Methodref #12.#33 // test/jvm/DynamicDispatch$Human.sayHello:()V
#7 = Class #34 // test/jvm/DynamicDispatch
#8 = Class #35 // java/lang/Object
#9 = Utf8 Woman
...剩下的常量池的内容省略了
{
public test.jvm.DynamicDispatch();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 21: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Ltest/jvm/DynamicDispatch;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #2 // class test/jvm/DynamicDispatch$Man
3: dup
4: invokespecial #3 // Method test/jvm/DynamicDispatch$Man."<init>":()V
7: astore_1
8: new #4 // class test/jvm/DynamicDispatch$Woman
11: dup
12: invokespecial #5 // Method test/jvm/DynamicDispatch$Woman."<init>":()V
15: astore_2
16: aload_1
17: invokevirtual #6 // Method test/jvm/DynamicDispatch$Human.sayHello:()V
20: aload_2
21: invokevirtual #6 // Method test/jvm/DynamicDispatch$Human.sayHello:()V
24: return
LineNumberTable:
line 44: 0
line 45: 8
line 46: 16
line 47: 20
line 48: 24
LocalVariableTable:
Start Length Slot Name Signature
0 25 0 args [Ljava/lang/String;
8 17 1 man Ltest/jvm/DynamicDispatch$Human;
16 9 2 woman Ltest/jvm/DynamicDispatch$Human;
}
0-7: 创建Man对象并将其存入局部变量表
8-15: 创建Woman对象并将其存入局部变量表
16: 将Man对象放入操作数栈顶
17: 取出栈顶Man对象调用虚方法, dispatch.sayHello(man); // Method test/jvm/DynamicDispatch$Human.sayHello:()V
20: 将Woman对象放入操作数栈顶
21: 取出栈顶Woman对象调用虚方法 dispatch.sayHello(woman); // Method test/jvm/DynamicDispatch$Human.sayHello:()V
这里的行号其实就是程序计数器执行完了一个指令后程序计数器下移继续执行下一行指令
我们可以看到字节码行号为17, 20的时候都是调用的Human.sayHello那么他是如何选定执行man还是woman的呢,这就要根据虚方法的访问规则来看了!具体如下
- 找到操作数栈顶第一个元素指向的对象标记为C
- 在C中寻找与之匹配的方法,如果找到了就返回其直接引用,但是因为访问权限这个方法不能访问会抛出IllegalAccessError异常。
- 如果没有找到就会对其父类进行第2步的查找
- 如果最终都没有找到合适的方法就会抛出java.lang.AbstractMethodError
看到这里相信大家都能明白了
当执行17行号的代码的时候此时操作数栈顶是Man对象,那么就会执行一遍以上的搜索成功调用Man的sayHello
同理调用Woman也是一样的