虚拟机字节码执行引擎之方法调用
引言
方法调用不是方法执行而是确定执行哪个方法。
解析
所有方法调用中的目标方法都是常量池中的一个符号引用,在类加载的解析阶段会将一部分符号引用转化为直接引用(方法入口地址),前提是方法在程序运行之前有明确的调用版本且运行期不可改变。这类方法的调用称为解析。
被invokestatic、invokespecial指令调用的方法(静态方法、实例构造器、私有方法、父类方法),符合上述条件,称为非虚方法。其中final方法也是非虚方法。
分派
静态分派
Human human = new Man(); “Human”称为静态类型,“Man”称为实际类型。静态类型编译期可知,实际类型运行期才可知。虚拟机在重载时以静态类型为依据而不是实际类型。
静态分派的典型应用是方法重载。
package com.wjz.demo; public class StaticDispatch { static abstract class Human { } static class Man extends Human { } static class Woman extends Human { } public void say(Human human) { System.out.println("Hello Human"); } public void say(Man man) { System.out.println("Hello Man"); } public void say(Woman woman) { System.out.println("Hello Woman"); } public static void main(String[] args) { StaticDispatch obj = new StaticDispatch(); Human man = new Man(); Human woman = new Woman(); obj.say(man); obj.say(woman); } }
输出结果
Hello Human
Hello Human
动态分派
动态分派的典型应用是重写。
package com.wjz.demo; public class DynamicDispatch { static abstract class Human { protected abstract void say(); } static class Man extends Human { protected void say() { System.err.println("Hello Man"); } } static class Woman extends Human { protected void say() { System.out.println("Hello Woman"); } } public static void main(String[] args) { Human man = new Man(); Human woman = new Woman(); man.say(); woman.say(); man = new Woman(); man.say(); } }
输出结果
Hello Man
Hello Woman
Hello Woman
使用javap输出代码的字节码。
0: new #2 // class com/wjz/demo/DynamicDispatch$Man 3: dup 4: invokespecial #3 // Method com/wjz/demo/DynamicDispatch$Man."<init>":()V 7: astore_1 8: new #4 // class com/wjz/demo/DynamicDispatch$Woman 11: dup 12: invokespecial #5 // Method com/wjz/demo/DynamicDispatch$Woman."<init>":()V 15: astore_2 16: aload_1 17: invokevirtual #6 // Method com/wjz/demo/DynamicDispatch$Human.say:()V 20: aload_2 21: invokevirtual #6 // Method com/wjz/demo/DynamicDispatch$Human.say:()V 24: new #4 // class com/wjz/demo/DynamicDispatch$Woman 27: dup 28: invokespecial #5 // Method com/wjz/demo/DynamicDispatch$Woman."<init>":()V 31: astore_1 32: aload_1 33: invokevirtual #6 // Method com/wjz/demo/DynamicDispatch$Human.say:()V 36: return
0~15行的字节码是准备动作,建立man和woman的内存空间,调用Man和Woman的实例构造器,将两个实例的引用放在局部变量表的第1、2个Slot中。
16和20行的字节码是将局部变量表的第1、2个Slot中的实例引用逐步压入栈顶。
17和21行的字节码是方法调用指令,从字节码角度看调用完全一样,但是输出结果不同。
invokespecial指令运行时的解析过程:
1)找到栈顶元素指向的对象的实际类型
2)找到符合方法,通过符号引用验证,返回方法的直接引用
3)否则,按照继承关系依次对父类搜索并验证
由于解析过程中查找到的实际类型不同,所以输出结果不同。
单分派与多分派
方法的接收者(静态类型的对象)和方法参数统称为方法的宗量。单分派是根据一个宗量对目标方法进行选择,多分派是基于多个。
静态分派是多分派,动态分派是单分派。
虚拟机动态分派的实现
为类在方法区创建一个虚方法表(Virtual Method Table,invokeinterface执行时会创建一个接口方发表),该类表中存放着各个方法的实际入口地址。如果子类没有重写父类的方法那么子类虚方法表中的地址入口和父类的一致,如果重写了父类的方法,子类表中的地址将会替换为子类实现版本的地址入口。
方法表一般在类加载的连接阶段进行初始化,准备了类变量后,虚拟机会把该类的方发表也初始完毕。