方法调用
2019-07-09 05:35:59 9 举报
描述jvm执行方法时,如何选择目标方法
作者其他创作
大纲/内容
分派调用
方法调用方式所处位置
需要说明的是方法调用与方法执行是不同的概念,方法调用的唯一任务就是确定被调用的方法到底是哪个方法(这里可以带着问题看这篇文档:一个子类被强转成父类,并执行相关方法,调用的是父类方法还是子类方法?)。我们知道在Class文件中存储的方法都是符号引用,不是方法在程序运行时的具体内存位置。符号引用的优点:这种特点给java带来了强大的动态扩展能力,比如在一个系统中分为两个模块,A模块依赖于B模块的某些类的方法,这时候即使B模块的类方法都是空实现的,不影响A模块的编译。且在系统运行期间B模块可以随时替换的。(当前我是这样理解的)符号引用的缺点:java方法调用过程变得相对复杂起来,需要在类加载期间,甚至到运行期间才能确定目标方法的直接引用。
方法调用
方法调用字节码
重写
单分派与多分派
所有方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,类加载期间可以解析的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。话句话说,调用目标在编译时就确定下来了。这类方法的调用称为解析。在java预言中符合\"编译期可知,运行期不可变\"这个要求的方法,主要包括静态方法和私有方法,前者直接和类型知己关联,后者在外部不可被访问,这两种方法各自的特点决定了它们都不可能通过继承或者别的方式重写其他版本,因此它们都适合在类加载阶段进行解析。
类生命周期
参考<<深入理解Java虚拟机>>
重载
方法调用:就是将上面提到的\"5种方法调用字节码指令\"所指向的符号引用转换成直接引用的过程。方法调用可以分为解析调用与分派调用两种方式,这里说明分派调用。解析调用是一个静态过程,编译期间就完全确定,在类装载的解析阶段就会把涉及的符号引用全部转换为可确定的直接引用,不会延迟到运行期间完成。而分派调用分为动态过程和静态过程,且根据宗量数分派调用还分为单分派和多分派,所以两两组合有静态单分派、静态多分派、动态单分派、动态多分派。java是面向对象语言,面向对象的语言有如下特性:继承、封装和多态那么多态的\"重载\"、\"重写\"在java虚拟机之中是如何实现的,且java虚拟机在多态环境下如何确定调用的是哪个方法。这就要使用分派调用。
虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。如果子类中重写了这个方法,子类方法表中的地址将会替换为指向子类实现版本的入口地址。图中,Son重写了来自Father的全部方法,因此Son的方法表没有指向Father类型数据的箭头。但是Son和Father都没有重写来自Object的方法,所以它们的方法表中所有从Object继承来的方法都指向了Object的数据类型。为了程序实现上的方便,具有相同签名的方法,在父类、子类的虚方法表中都应当具有一样的索引序号,这样当类型变换时,仅需要变更查找的方法表,就可以从不同的虚方法表中按索引转换出所需的入口地址。方法表一般在类加载的连接阶段进行初始化,准备了类的变量初始值后,虚拟机会把该类的方法表也初始化完毕。
代码:public class Demo { private void privateMethodDemo(){ } public final void finalMethodDemo(){ } public static void staticMethodDemo(){ } public static void main(String[] args){ //实例构造器 Demo Demo = new Demo(); //调用静态方法 Demo.staticMethodDemo(); //私有方法 Demo.privateMethodDemo(); //调用父类方法 Demo.toString(); //调用final方法 Demo.finalMethodDemo(); }}执行javap命令javap -v Demo.class
方法的接收者与方法的参数统称为方法的宗量,这个定义最早应该来源于《Java与模式》一书。根据分派基于多少种宗量,可以将分派划分为单分派和多分派两种。单分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行选择。java的静态多分派与动态单分派,静态多分派,具体来说是在编译期通过对象的静态类型与方法类型选择目标方法。动态单分派具体来说是,在运行期执行静态多分派选择的目标方法时,由于编译期已经决定目标方法的签名,虚拟机此时不会关心方法与方法参数,因为这些参数都固定了、实际类型都对方法的选择不会构成任何影响,唯一可以影响虚拟机选择的因素只有此方法的接受者的实际类型。因为只有一个宗量作为选择依据,所以Java语言的动态分派属于单分派类型。
类加载
编译
解析
由于动态分派是非常频繁的动作,而且动态分派的方法版本选择过程需要运行时在类的方法元数据中搜索合适的目标方法,因此在虚拟机的实际实现中基于性能的考虑,大部分实现都不会真正地进行如此频繁的搜索。面对这种情况,最常用的“稳定优化”手段就是为类在方法区中建立一个虚方法表(Vritual Method Table,也称为vtable,与此对应的,在invokeinterface执行时也会用到接口方法表——Inteface Method Table,简称itable),使用虚方法表索引来代替元数据查找以提高性能。我们先上面重写代码所对应的虚方法表结构示例:
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\
非虚方法使用时字节码
虚拟机动态分派的实现
解析与分派这两者之间的关系并不是二选一的排他关系,它们是在不同层次上去筛选、确定目标方法的过程。例如,静态方法会在类加载期就进行解析,而静态方法显然也是可以拥有重载版本的,选择重载版本的过程也是通过静态分派完成的。
动态分派
实例
静态分派
java虚拟机提供了5种方法调用字节码指令:1.invokestatic:调用静态方法。2.invokespecial:调用实例构造器<init>方法、私有方法和父类方法。3.invokevirtual:调用所有的虚方法。4.invokeinterface:调用接口方法,会在运行时确定一个实现此接口的对象。5.invokedynamic:先在运行时动态解析出\"调用点限制符\"所引用的方法,然后再执行该方法,在此之前的4条指令,分派逻辑是\"固化在\"java虚拟机内部的,而invokedynamic指令的\"分派逻辑\"是由用户所设定的引导方法决定的。(这里说明有些抽象,随后会详细说明)
方法版本唯一
运行
java语法层面
0 条评论
下一页