【JVM】静态分派和动态分派

xiaoxiao2021-02-28  52

分派

  Java是一门面向对象的程序语言,同时Java也是具备3个基本特征的:继承、封装和多态。而分派则是多态性特征的最基本的体现。开始之前我们要先了解两个概念:

静态类型(Static Type)或者叫做外观类型(Apparent Type),即是变量声明时的类型 实际类型(Actual Type),变量实例化时采用的类型

静态分派

  所有依赖静态类型来定位方法执行版本的分派动作,都称为静态分派,静态分派的最典型应用就是多态性中的方法重载(Overload)。静态分派发生在编译阶段,因此确定静态分配的动作实际上不是由虚拟机来执行的。下面通过一段方法重载的示例程序来更清晰地说明这种分派机制:

class Human{ } class Man extends Human{ } class Woman extends Human{ } public class StaticDispatch{ public void say(Human hum){ System.out.println("I am human"); } public void say(Man hum){ System.out.println("I am man"); } public void say(Woman hum){ System.out.println("I am woman"); } public static void main(String[] args){ Human man = new Man(); Human woman = new Woman(); StaticDispatch sr = new StaticDispatch(); sr.say(man); sr.say(woman); } } 上面代码的执行结果如下:

    I am human     I am human

先看如下代码: Human man = new Man();   我们把上面代码中的“Human”称为变量的静态类型,后面的“Man”称为变量的实际类型。静态类型和实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是在编译期可知的,而实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。

  回到上面的代码分析中,在调用say()方法时,方法的调用者都为sr的前提下,使用哪个重载版本,完全取决于传入参数的数量和数据类型。代码中刻意定义了两个静态类型相同、实际类型不同的变量,可见编译器在重载时是通过参数的静态类型而不是实际类型作为判定依据的。并且静态类型是编译期可知的,所以在编译阶段,Javac编译器就根据参数的静态类型决定使用哪个重载版本。

  在《深入理解Java虚拟机——JVM高级特性与最佳实践》中还有一个关于重载方法匹配优先级的代码,感兴趣的童鞋可以去看看。

动态分派

  动态分派与多态性的另一个重要体现——重写(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   接下来看一下javap命令输出的这个main()方法的字节码,从中分析一下 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/DynamicDispatch $Man."<init>":()V 7:astore_1 8:new#19;//class org/fenixsoft/polymorphic/DynamicDispatch $Woman 11:dup 12:invokespecial#21;//Method org/fenixsoft/polymorphic/DynamicDispa tch $Woman."<init>":()V 15:astore_2 16:aload_1 17:invokevirtual#22;//Method org/fenixsoft/polymorphic/DynamicDispatch $Human.sayHello:()V 20:aload_2 21:invokevirtual#22;//Method org/fenixsoft/polymorphic/DynamicDispatch $Human.sayHello:()V 24:new#19;//class org/fenixsoft/polymorphic/DynamicDispatch $Woman 27:dup 28:invokespecial#21;//Method org/fenixsoft/polymorphic/Dynam icDispatch $Woman."<init>":()V 31:astore_1 32:aload_1 33:invokevirtual#22;//Method org/fenixsoft/polymorphic/ DynamicDispatch $Human.sayHello:()V 36:return   0~15行的字节码是准备动作,作用是建立man和woman的内存空间、 调用Man和Woman类型的实例构造器,将这两个实例的引用存放在第1、 2个局部变量表Slot之中,这个动作也就对应了代码中的这两句: Human man=new Man(); Human woman=new Woman();   接下来的16~21句是关键部分,16、 20两句分别把刚刚创建的两个对象的引用压到栈顶,这两个对象是将要执行的sayHello()方法的所有者,称为接收者(Receiver);17和21句是方法调用指令,这两条调用指令单从字节码角度来看,无论是指令(都是invokevirtual)还是参数(都是常量池中第22项的常量,注释显示了这个常量是Human.sayHello()的符号引用)完全一样的,但是这两句指令最终执行的目标方法并不相同。 原因就需要从invokevirtual指令的多态查找过程开始说起,invokevirtual指令的运行时解析过程大致分为以下几个步骤: 1)找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。 2)如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常。 3)否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。 4)如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。   由于invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,所以两次调用中的invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上,这个过程就是Java语言中方法重写的本质。 我们把这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。
转载请注明原文地址: https://www.6miu.com/read-600086.html

最新回复(0)