一起学Java虚拟机系列:
- 一起学Java虚拟机(一):内存区域和垃圾收集
- 一起学Java虚拟机(二):类文件结构
- 一起学Java虚拟机(三):字节码指令简介
- 一起学Java虚拟机(四):类加载机制
- 一起学Java虚拟机(五):运行时栈帧结构
方法调用的任务
方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还未涉及方法内部的具体运行过程。
Java Class文件的编译过程中不包含传统程序语言编译的连接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(也就是之前说的直接引用)。
这个特性给Java带来了更强大的动态扩展能力,但也使得Java方法调用过程变得相对复杂,某些调用需要在类加载期间,甚至到运行期间才能确定目标方法的直接引用。
解析
方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。换句话说,调用目标在程序代码写好、编译器进行编译那一刻就已经确定下来。这类方法
的调用被称为解析(Resolution)。
非虚方法
静态方法、私有方法、实例构造器、父类方法、被final修饰的方法,这5种方法调用会在类加载的时候就可以把符号引用解析为该方法的直接引用。这些方法统称为“非虚方法”(Non-Virtual Method),与之相反,其他方
法就被称为“虚方法”(Virtual Method)
解析调用一定是个静态的过程,在编译期间就完全确定,在类加载的解析阶段就会把涉及的符号 引用全部转变为明确的直接引用,不必延迟到运行期再去完成。
方法调用字节码指令
invokestatic: 用于调用静态方法
invokespecial: 用于调用实例构造器()方法、私有方法和父类中的方法
invokevirtual: 用于调用所有的虚方法
invokeinterface: 用于调用接口方法,会在运行时再确定一个实现该接口的对象
invokedynamic: 先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法
非虚方法调用示例
public class HelloWorld {
public static void main(String args[]) {
sayHello();
}
public static void sayHello() {
System.out.println("Hello World");
}
}
javap 查看字节码,发现是通过invokestatic命令调用sayHello方法,所以其调用方法的版本再编译期就已经明确,以常量池项的形式固化再字节码指令的参数中
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=1, args_size=1
0: invokestatic #2 // Method sayHello:()V
3: return
LineNumberTable:
line 6: 0
line 7: 3
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 args [Ljava/lang/String;
分派
静态分派
虚拟机(或者准确地说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据的
所有依赖静态类型来决定方法执行版本的分派动作,都称为静态分派。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。
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,gentleman!");
}
public void sayHello(Woman guy) {
System.out.println("hello,lady!");
}
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”称为变量的“静态类型”(Static Type),或者叫“外观类 型”(Apparent Type),后面的“Man”则被称为变量的“实际类型”(Actual Type)或者叫“运行时类 型”(Runtime Type)。
动态分派
动态分派的实现过程,与Java语言多态性的另外一个重要体现重写(Override)有着很密切的关联
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 = new Woman();
man.sayHello();
}
}
运行结果:
man say hello
woman say hello
woman say hello
字节码
public static void main(java.lang.String[]);
Code:
Stack=2, Locals=3, Args_size=1
0: new #16; //class org/fenixsoft/polymorphic/DynamicDispatch$Man
3: dup
4: invokespecial #18; //Method org/fenixsoft/polymorphic/Dynamic Dispatch$Man."<init>":()V
7: astore_1
8: new #19; //class org/fenixsoft/polymorphic/DynamicDispatch$Woman
11: dup
12: invokespecial #21; //Method org/fenixsoft/polymorphic/DynamicDispatch$Woman."<init>":()V
15: astore_2
16: aload_1
17: invokevirtual #22; //Method org/fenixsoft/polymorphic/Dynamic Dispatch$Human.sayHello:()V
20: aload_2
21: invokevirtual #22; //Method org/fenixsoft/polymorphic/Dynamic Dispatch$Human.sayHello:()V
24: new #19; //class org/fenixsoft/polymorphic/DynamicDispatch$Woman
27: dup
28: invokespecial #21; //Method org/fenixsoft/polymorphic/DynamicDispatch$Woman."<init>":()V
31: astore_1
32: aload_1
33: invokevirtual #22; //Method org/fenixsoft/polymorphic/Dynamic Dispatch$Human.sayHello:()V
36: return
从字节码角度来看,无论是指令(都是invokevirtual)还是参数(都是常量池中第22项的常量,注释显示了这个常量是Human.sayHello()的符号引用)都完全一样,但是这两句指令最终执行的目标方法并不相同。 所以关键点还是invokevirtual指令本身如何确认方法版本、实现多态查找。
invokevirtual指令的运行时解析过程
- 找到操作数栈顶的第一个元素所指向的对象的
实际类型,记作C。- 如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方> 法的直接引用,查找过程结束;不通过则返回java.lang.IllegalAccessError异常。
- 否则,按照继承关系从下往上依次对C的各个父类进行第二步的搜索和验证过程。
- 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。
因为invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,所以两次调用中的invokevirtual指令并不是把常量池中方法的符号引用解析到直接引用上就结束了,还会根据方法接收者的实际类型来选择方法版本,这个过程就是Java语言中方法重写的本质。 我们把这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。
既然这种多态性的根源在于虚方法调用指令invokevirtual的执行逻辑,那自然我们得出的结论就只
会对方法有效,对字段是无效的,因为字段不使用这条指令。当子类声明了与父类同名的字段时,虽然在子类的内存中两个字段都会存在,但是子类的字段会遮蔽父类的同名字段。
单分派与多分派
方法的接收者与方法的参数统称为方法的宗量。
多分派
编译阶段中编译器的选择过程,也就是静态分派的过程选择目标方法的依据有两点,静态类型和方法参数, 所以是多分派类型。
单分派
看运行阶段中虚拟机的选择,也就是动态分派的过程,由于编译器已经决定了目标方法的签名,只有一个宗量方法接收者(实际类型)作为选择依据,所以属于单分派类型。