Java的动态分派和静态分派的实现
Java方法执行时的动态分派和静态分派是Java实现多态的本质
背景
Java的动态分派和静态分派也是Java方法的执行原理。Java源代码的编译之后,方法之间的调用是使用符号引用来表示的。当字节码被JVM加载之后,符号引用才会被替换为对应方法在方法区的真实内存地址。那么在替换之前,由于Java的方法重写、重载,就导致符号引用对应的方法可能是一个虚方法,那么方法的真实实现在运行时就可能有多个。
所以在将符号引用替换为真实地址时,还需要做一件事情:那就是确定符号引用要替换的方法的版本。
运行时方法帧
与C,C++一样,JVM在运行时也会维护一个运行栈,用于方法的调用和返回。当调用一个方法时,会为方法在栈上分配一块内存区域作为方法的帧。方法调用帧又分为下面几个区域:
局部变量表
存储方法参数和方法体中的局部变量,其容量在编译期就已确定。容量的最小单位是variableslot(变量槽)。
静态方法的局部变量数就是方法体中声明的变量数;实例方法的局部变量数会多一个,多出的一个就是我们平时在实例方法中访问的this。this其实是编译器在编译时悄悄加到实例方法上的,而且是作为第一个参数。
操作数栈
JVM的字节码指令执行机制是基于栈的,所以需要一个栈来存储字节码指令的操作数。
Android的VM是基于寄存器的,所以没有操作栈区域。
AndroidVM采用寄存器存储操作数有两个主要原因:1.寄存器乃是CPU内部的高速内存,读写寄存器是与CPU交互最快的方式。2.智能手机多使用ARM架构的CPU,ARM架构的CPU有很多通用寄存器可使用。
动态链接
方法体中调用其他方法时,会把将要调用的方法在常量池中的符号引用,转化为将要其在方法区内存中的开始地址信息,并储存到动态链接中。
方法返回地址
一个方法执行完毕之后,线程需要值得回到哪里继续执行,方法返回地址就是存储这个信息的。返回地址一般就是当前方法的调用者的程序计数器的值(PC寄存器)。
- 正常完成出口:方法正常返回时,如果有返回值,返回值会被压入调用方法的操作数栈中
- 异常完成出口:当方法发生了异常,且在异常表中没有找到匹配的异常处理流程时,方法将不会有返回值
方法调用
方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法)
调用方法的指令
有以下字节码指令用于方法的调用:
指令 | 用途 | 说明 |
---|---|---|
invokestatic | 调用类的静态方法 | |
invokespecfical | 调用对象的构造函数和私有方法 | |
invokevirtual | 调用对象的public/protected的方法 | 可能通过继承复写的方法称做virtualmethod:表示要到运行时才能定位到真正的方法实现。通过符号引用确定虚方法直接引用的过程又叫做动态分派 |
invokeinterface | 调用接口的方法 | 具体的实现类将在调用时确定 |
invokedynamic | JDK1.7为了让JVM支持动态类型语言引入的指令 | 让用户可以决定如何查找目标方法 |
符号引用到直接引用
由于Java的编译没有CC++编译过程中的链接阶段,所以Class文件中储存的只是符号引用,等到了在运行时才通过符号引用定位到方法区中方法代码在内存布局中的位置--直接引用。
符号引用到直接引用的替换又涉及两种方式。一种是解析,另一种是分派。解析发生在类加载的解析阶段,分派发生在编译或方法调用阶段。
解析
在类加载的解析阶段会把满足「编译期可知,运行期不可变」的方法的符号引用替换为指向方法区的直接引用,不会延迟到运行时再去完成。
满足编译期可知,运行期不可变的方法有:构造函数、私有方法、静态方法、final修饰的方法。不满足上述条件的方法的符号引用替换发生在方法调用期间。
分派Dispatch
多态的实现原理
变量类型
理解分派之前,需要先看两个类型概念。
比如:Objectobj=newString("");
静态类型
定义变量时,声明的类型。比如这里obj的静态类型就是Object。静态类型在编译期的编译器就能知道。
实际类型
变量赋值时的实际类型。比如这里obj的实际类型就是String。实际类型在编译期的编译器是不可知的。
静态分派
根据变量的「静态类型(外观类型)」匹配调用方法的过程称为静态分派。发生的场景为方法重载。
如下代码:
publicclassStaticDispatch{ staticabstractclassHuman{} staticclassManextendsHuman{} staticclassWomanextendsHuman{} staticclassChildextendsHuman{} publicvoidsay(Humanhuman){ System.out.println("human"); } publicvoidsay(Manman){ System.out.println("man"); } publicvoidsay(Womanwoman){ System.out.println("woman"); } publicvoidsay(Childchild){ System.out.println("child"); } }
publicstaticvoidmain(String[]args){ Humanman=newMan(); Humanwoman=newWoman(); Humanchild=newChild(); StaticDispatchdispatch=newStaticDispatch(); dispatch.say(man); dispatch.say(woman); dispatch.say(child); }
main方法的执行结果:
human
human
human
虽然StaticDispatch为每种Human的子类都重载了一个say方法,但是由于重载采用的是静态分派,是根据对象的静态类型做方法匹配的。所以结果全都匹配到了publicvoidsay(Humanhuman)方法。main方法编译之后的字节码:
publicstaticmain([Ljava/lang/String;)V NEWmethod_invoke/StaticDispatch$Man DUP INVOKESPECIALmethod_invoke/StaticDispatch$Man.()V ASTORE1 NEWmethod_invoke/StaticDispatch$Woman DUP INVOKESPECIALmethod_invoke/StaticDispatch$Woman. ()V ASTORE2 NEWmethod_invoke/StaticDispatch$Child DUP INVOKESPECIALmethod_invoke/StaticDispatch$Child. ()V ASTORE3 NEWmethod_invoke/StaticDispatch DUP INVOKESPECIALmethod_invoke/StaticDispatch. ()V ASTORE4 //下面为调用say ALOAD4 ALOAD1 INVOKEVIRTUALmethod_invoke/StaticDispatch.say(Lmethod_invoke/StaticDispatch$Human;)V ALOAD4 ALOAD2 INVOKEVIRTUALmethod_invoke/StaticDispatch.say(Lmethod_invoke/StaticDispatch$Human;)V ALOAD4 ALOAD3 INVOKEVIRTUALmethod_invoke/StaticDispatch.say(Lmethod_invoke/StaticDispatch$Human;)V RETURN
从字节码也能看到,编译器确实是按照静态分派选择了匹配静态类型的StaticDispatch.say(LStaticDispatch$Human;)V方法,而没有按照变量的实际类型去匹配重载的方法。
publicclassOverload{ publicstaticvoidout(chara){System.out.println("char"+a);} publicstaticvoidout(inta){System.out.println("int"+a);} publicstaticvoidout(longa){System.out.println("long"+a);} publicstaticvoidout(floata){System.out.println("float"+a);} publicstaticvoidout(doublea){System.out.println("double"+a);} publicstaticvoidout(Integera){System.out.println("integer");} publicstaticvoidout(Charactera){System.out.println("character");} publicstaticvoidout(Serializablea){System.out.println("serializable"+a);} publicstaticvoidout(Comparablea){System.out.println("comparable"+a);} publicstaticvoidout(Objecta){System.out.println("object"+a);} publicstaticvoidout(char...a){System.out.println("char..."+Arrays.toString(a));} publicstaticvoidmain(String[]args){ out('c'); } }
这段代码也是一个静态分派的例子,编译器会选择参数类型做合适的函数去调用。可以注释掉所有out函数,留下out(Serializablea),你会发现程序也能成功编译和运行。如果留下Serializeable和Comparable编译则会失败,提示对out的引用不明确。
动态分派
根据变量的「实际类型」匹配调用方法的过程称为动态分派。发生的场景为方法重写。当调用一个可能被子类重写或继承的方法时,就会触发动态分派。
publicclassDynamicDispatch{ staticclassHuman{ publicvoidsay(){ System.out.println("human"); } } staticclassManextendsHuman{ @Override publicvoidsay(){ System.out.println("man"); } } staticclassWomanextendsHuman{ @Override publicvoidsay(){ System.out.println("woman"); } } }
publicstaticvoidmain(String[]args){ Humanhuman=newHuman(); Humanman=newMan(); Humanwoman=newWoman(); human.say(); man.say(); woman.say(); }
main方法的执行结果:
human
man
woman
意料之中,所谓的多态就是这样。那多态是如何实现的?
其实多态的实现过程也就是确定被重写的方法版本的过程。main方法编译之后的字节码:
publicstaticmain([Ljava/lang/String;)V NEWmethod_invoke/DynamicDispatch$Human DUP INVOKESPECIALmethod_invoke/DynamicDispatch$Human.()V ASTORE1 NEWmethod_invoke/DynamicDispatch$Man DUP INVOKESPECIALmethod_invoke/DynamicDispatch$Man. ()V ASTORE2 NEWmethod_invoke/DynamicDispatch$Woman DUP INVOKESPECIALmethod_invoke/DynamicDispatch$Woman. ()V ASTORE3 //下面为多态调用say ALOAD1 INVOKEVIRTUALmethod_invoke/DynamicDispatch$Human.say()V ALOAD2 INVOKEVIRTUALmethod_invoke/DynamicDispatch$Human.say()V ALOAD3 INVOKEVIRTUALmethod_invoke/DynamicDispatch$Human.say()V RETURN
这里通过字节码感觉都会调用Hunman#say方法的,但是运行之后并不是。
当JVM执行这两行字节码时:
ALOAD1 //由上面ASTORE1可知,局部变量表的第一个变量是Woman的对象 INVOKEVIRTUALmethod_invoke/DynamicDispatch$Human.say()V //INVOKEVIRTUAL指令就会到Woman类中去寻找say方法
调用say方法时,JVM会先去当前调用的对象的类中查找是否存在和目标方法的描述符、简单名称一样的方法,如果存在则将符号引用替换为找到的方法的直接引用,否则就向父类去查找,向父类的父类去查找...,直到最后找不到抛出NoSuchMethod异常。
Human的say方法的签名:
publicvoidsay(); descriptor:()V
Woman的say方法的签名:
publicvoidsay(); descriptor:()V
可见Woman类的Human类中的say方法的描述符和简单名称是一样的,所以JVM会优先匹配Woman类中的方法。这也是多态调用的底层逻辑。
到此这篇关于Java的动态分派和静态分派的实现的文章就介绍到这了,更多相关Java动态分派和静态分派内容请搜索毛票票以前的文章或继续浏览下面的相关文章希望大家以后多多支持毛票票!
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。