摘抄深入理解java虚拟机
目标
1. 静态分派
2. 动态分派
3. 单分派与多分派
分派包括: 静态分派与动态分派两种类型, 静态分派的使用场景为重载, 动态分派使用场景为重写.
类型 | 方法调用依据 | 发生阶段 | 应用场景 |
---|---|---|---|
静态分派 | 方法中参数的静态类型 | 编译期 | 重载 |
动态分派 | 方法调用者的实际类型 | 运行期 | 重写 |
根据两者的区别分别分析静态分派
与动态分派
.
一、方法调用
方法调用并不等同于方法执行
, 方法调用阶段唯一的任务
就是确定被调用方法的版本(即调用哪一个方法), 暂时还不涉及方法内部的具体运行过程. 一切方法调用在Class文件里面存储的都只是符号引用, 而不是方法在实际运行时内存布局中的入口地址(相当于之前说的直接引用).
1.1 解析
所有方法调用中的目标方法在Class文件里面都是一个常量池的符号引用, 在类加载的解析阶段, 会将其中的一部分符号引用转化为直接引用, 这种解析能成立的前提是: 方法在程序真正运行之前就有一个可确定的调用版本, 并且这个方法的调用版本在运行期是不可改变的. 换句话说, 调用目标在程序代码写好、编译器进行编译时就必须确定下来, 这类方法的调用称为解析(Resolution)
.
二、静态分派
使用场景: 重载
public class StaticDispatch {
static abstract class Human {}
static class Man extends Human {}
static class Woman extends Human {}
public void sayHello(Human guy) {
System.out.println("hello, guy!");
}
public void sayHello(Man guy) {
System.out.println("hello, Man!");
}
public void sayHello(Woman guy) {
System.out.println("hello, Man!");
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
StaticDispatch sr = new StaticDispatch();
sr.sayHello(man);
sr.sayHello(woman );
}
}
输出结果:
hello, guy!
hello, guy!
Human
称为变量的静态类型, Man
称为变量的实际类型, 静态类型是在编译期可知的, 实际类型变化的结果在运行期才可确定, 编译器在编译程序的时候并不知道一个对象的实际类型是什么.
关于重载
, 运行时使用哪个重载版本, 完全取决于传入参数的数量和数据类型, 编译器在重载时是通过参数的静态类型而不是实际类型作为判断依据的, 并且静态类型是编译期可知的, 因此在编译阶段, Javac编译器会根据参数的静态类型决定使用哪个重载版本, 所以选择了sayHello(Human)
作为调用目标, 并把这个方法的符号引用写到main()
方法里面的两条invokevirtual指令的参数中(关于这段话, 通过下文字节码文件可以更好的理解)
.
感觉书里面写的好像还是有点儿晦涩, 然后找到了一个概念摘抄自RednaxelaFX, 函数重载是编译时概念, 参数类型是由变量声明的类型所决定的.
然后用javap -verbose打开上面的字节码文件:
字节码文件比较长, 只列出关键的几项
public class algorithm.test.StaticDispatch
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #13.#43 // java/lang/Object."<init>":()V
#2 = Fieldref #44.#45 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #46 // hello, guy!
#4 = Methodref #47.#48 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = String #49 // hello, Man!
#6 = Class #50 // algorithm/test/StaticDispatch$Man
#7 = Methodref #6.#43 // algorithm/test/StaticDispatch$Man."<init>":()V
#8 = Class #51 // algorithm/test/StaticDispatch$Woman
#9 = Methodref #8.#43 // algorithm/test/StaticDispatch$Woman."<init>":()V
#10 = Class #52 // algorithm/test/StaticDispatch
#11 = Methodref #10.#43 // algorithm/test/StaticDispatch."<init>":()V
#12 = Methodref #10.#53 // algorithm/test/StaticDispatch.sayHello:(Lalgorithm/test/StaticDispatch$Human;)V
#13 = Class #54 // java/lang/Object
#14 = Utf8 Woman
#15 = Utf8 InnerClasses
#16 = Utf8 Man
#17 = Class #55 // algorithm/test/StaticDispatch$Human
#18 = Utf8 Human
#19 = Utf8 <init>
#20 = Utf8 ()V
#21 = Utf8 Code
#22 = Utf8 LineNumberTable
#23 = Utf8 LocalVariableTable
#24 = Utf8 this
#25 = Utf8 Lalgorithm/test/StaticDispatch;
#26 = Utf8 sayHello
#27 = Utf8 (Lalgorithm/test/StaticDispatch$Human;)V
#28 = Utf8 guy
#29 = Utf8 Lalgorithm/test/StaticDispatch$Human;
#30 = Utf8 (Lalgorithm/test/StaticDispatch$Man;)V
#31 = Utf8 Lalgorithm/test/StaticDispatch$Man;
#32 = Utf8 (Lalgorithm/test/StaticDispatch$Woman;)V
#33 = Utf8 Lalgorithm/test/StaticDispatch$Woman;
#34 = Utf8 main
#35 = Utf8 ([Ljava/lang/String;)V
#36 = Utf8 args
#37 = Utf8 [Ljava/lang/String;
#38 = Utf8 man
#39 = Utf8 woman
#40 = Utf8 sr
#41 = Utf8 SourceFile
#42 = Utf8 StaticDispatch.java
#43 = NameAndType #19:#20 // "<init>":()V
#44 = Class #56 // java/lang/System
#45 = NameAndType #57:#58 // out:Ljava/io/PrintStream;
#46 = Utf8 hello, guy!
#47 = Class #59 // java/io/PrintStream
#48 = NameAndType #60:#61 // println:(Ljava/lang/String;)V
#49 = Utf8 hello, Man!
#50 = Utf8 algorithm/test/StaticDispatch$Man
#51 = Utf8 algorithm/test/StaticDispatch$Woman
#52 = Utf8 algorithm/test/StaticDispatch
#53 = NameAndType #26:#27 // sayHello:(Lalgorithm/test/StaticDispatch$Human;)V
#54 = Utf8 java/lang/Object
#55 = Utf8 algorithm/test/StaticDispatch$Human
#56 = Utf8 java/lang/System
#57 = Utf8 out
#58 = Utf8 Ljava/io/PrintStream;
#59 = Utf8 java/io/PrintStream
#60 = Utf8 println
#61 = Utf8 (Ljava/lang/String;)V
{
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: new #6 // class algorithm/test/StaticDispatch$Man
3: dup
4: invokespecial #7 // Method algorithm/test/StaticDispatch$Man."<init>":()V
7: astore_1
8: new #8 // class algorithm/test/StaticDispatch$Woman
11: dup
12: invokespecial #9 // Method algorithm/test/StaticDispatch$Woman."<init>":()V
15: astore_2
16: new #10 // class algorithm/test/StaticDispatch
19: dup
20: invokespecial #11 // Method "<init>":()V
23: astore_3
24: aload_3
25: aload_1
26: invokevirtual #12 // Method sayHello:(Lalgorithm/test/StaticDispatch$Human;)V
29: aload_3
30: aload_2
31: invokevirtual #12 // Method sayHello:(Lalgorithm/test/StaticDispatch$Human;)V
34: return
LineNumberTable:
line 27: 0
line 28: 8
line 29: 16
line 30: 24
line 31: 29
line 32: 34
LocalVariableTable:
Start Length Slot Name Signature
0 35 0 args [Ljava/lang/String;
8 27 1 man Lalgorithm/test/StaticDispatch$Human;
16 19 2 woman Lalgorithm/test/StaticDispatch$Human;
24 11 3 sr Lalgorithm/test/StaticDispatch;
}
上面重点关注main()里面的26
和31
两个指令, 然后对应Class常量池中的#12
, 然后再反过来理解上文中摘抄的那段话, 编译期确定调用目标, 然后把对应方法的符号引用写到main()方法的两条invokevirtual指令的参数中.
通过Class的Constant pool:可以看到26和31的调用路径为: #12 -> #10、#53 -> #52、#26、#27, 结合这个调用链可以很直观的看到: 被确定的目标方法为sayHello(Human), 其对应的符号引用为#53, 而#53又被写入到了#12中, 而main方法中的invokevirtual指令对应常量池中的符号引用#12
三、动态分派
使用场景: 重写
是用动态分派还是静态分派?
向上转型后调用子类覆写的方法便是一个很好地说明动态分派的例子, 此时在判断执行父类中的方法还是子类中覆盖的方法时, 如果用静态类型来判断, 那么无论怎么进行向上转型, 都只会根据静态类型来进行判断, 然后只会调用父类中的方法, 但实际情况是根据父类实例化的子类不同, 调用的是不同子类中覆写的方法, 这里要根据变量的实际类型来分派方法的执行版本, 而实际类型的确定需要在程序运行时才能确定下来, 这里在运行期根据实际类型确定方法执行版本的分派过程称为动态分派.
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 -verbose DynamicDispatch.class
public class algorithm.test.DynamicDispatch
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #8.#30 // java/lang/Object."<init>":()V
#2 = Class #31 // algorithm/test/DynamicDispatch$Man
#3 = Methodref #2.#30 // algorithm/test/DynamicDispatch$Man."<init>":()V
#4 = Class #32 // algorithm/test/DynamicDispatch$Woman
#5 = Methodref #4.#30 // algorithm/test/DynamicDispatch$Woman."<init>":()V
#6 = Methodref #12.#33 // algorithm/test/DynamicDispatch$Human.sayHello:()V
#7 = Class #34 // algorithm/test/DynamicDispatch
#8 = Class #35 // java/lang/Object
#9 = Utf8 Woman
#10 = Utf8 InnerClasses
#11 = Utf8 Man
#12 = Class #36 // algorithm/test/DynamicDispatch$Human
#13 = Utf8 Human
#14 = Utf8 <init>
#15 = Utf8 ()V
#16 = Utf8 Code
#17 = Utf8 LineNumberTable
#18 = Utf8 LocalVariableTable
#19 = Utf8 this
#20 = Utf8 Lalgorithm/test/DynamicDispatch;
#21 = Utf8 main
#22 = Utf8 ([Ljava/lang/String;)V
#23 = Utf8 args
#24 = Utf8 [Ljava/lang/String;
#25 = Utf8 man
#26 = Utf8 Lalgorithm/test/DynamicDispatch$Human;
#27 = Utf8 woman
#28 = Utf8 SourceFile
#29 = Utf8 DynamicDispatch.java
#30 = NameAndType #14:#15 // "<init>":()V
#31 = Utf8 algorithm/test/DynamicDispatch$Man
#32 = Utf8 algorithm/test/DynamicDispatch$Woman
#33 = NameAndType #37:#15 // sayHello:()V
#34 = Utf8 algorithm/test/DynamicDispatch
#35 = Utf8 java/lang/Object
#36 = Utf8 algorithm/test/DynamicDispatch$Human
#37 = Utf8 sayHello
{
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 algorithm/test/DynamicDispatch$Man
3: dup
4: invokespecial #3 // Method algorithm/test/DynamicDispatch$Man."<init>":()V
7: astore_1
8: new #4 // class algorithm/test/DynamicDispatch$Woman
11: dup
12: invokespecial #5 // Method algorithm/test/DynamicDispatch$Woman."<init>":()V
15: astore_2
16: aload_1
17: invokevirtual #6 // Method algorithm/test/DynamicDispatch$Human.sayHello:()V
20: aload_2
21: invokevirtual #6 // Method algorithm/test/DynamicDispatch$Human.sayHello:()V
24: return
LineNumberTable:
line 24: 0
line 25: 8
line 26: 16
line 27: 20
line 28: 24
LocalVariableTable:
Start Length Slot Name Signature
0 25 0 args [Ljava/lang/String;
8 17 1 man Lalgorithm/test/DynamicDispatch$Human;
16 9 2 woman Lalgorithm/test/DynamicDispatch$Human;
}
invokevirtual指令的运行时解析过程大致分为以下几个步骤:
- 1、找到操作数栈顶的第一个元素所指向的对象的实际类型, 记作C
- 2、如果类型C中找到与常量中的描述符和简单名称都相符的方法, 则进行访问权限校验, 如果通过则返回这个方法的直接引用, 查找过程结束
- 3、否则, 按照继承关系从下往上依次对C的各个父类件第二步的搜索和验证过程
分析一下DynamicDispatch.class
的main方法的流程
- 1、<0、3、4>完成的流程为new Man()
- 2、<7>完成变量的装载操作, astore_1对应LocalVariableTable中Slot_1, 也就是将new Man()指向Human man
- 3、<0、3、4、7>完成Human man = new Man()的操作
- 4、同理<8、11、12、15>完成Human woman = new Woman()的操作
- 5、接下来看<16、17>, 16是aload_1也就是将Slot_1对应的man压入到栈顶, 然后结合invokevirtual的第二点, 从栈顶元素(这里也就是aload_1压入的Human man元素实际指向的是new Man)中查找是否存在目标方法, 如果存在则校验, 通过后直接返回该方法的直接引用, 查找结束.
- 6、<20、21>操作桶<16、17>操作
参考文章:
https://blog.csdn.net/pange1991/article/details/82080907
网友评论