- 方法调用并不等于方法执行,方法调用的唯一目的就是确定被调用方法的版本。在程序运行中,进行方法调用是最普遍,最频繁的操作,但是在Class文件中存储的都是符号引用,而不是实际方法在内存中实际的入口地址(直接引用)。而且我们知道java中是支持方法的重载与重写的,那么根据符号引用找到正确的方法(直接引用)进行调用的工作就显得更加的复杂与困难了。
- 虚拟机的方法调用中用到的方法有解析、静态分派、动态分派等,及虚方法表,接口方法表等技术。接下来会 一 一 的介绍。
- 我们了解虚拟机中方法调用的原理也有利与我们理解java语言中多态与继承的特性。
解析
- 所有方法的调用中的目标方法在Class文件里面都是一个常量池的符号引用,在类加载阶段,一部分的符号引用可以被直接解析为直接引用。这种解析能成立的必要条件是:方法在运行之前就有一个可确定的版本,并且这个版本在运行期间不可改变的。也就是“编译器可知,运行期不可变”,这类方法的调用就称为解析。
- 从方法属性来说主要有静态方法,私有方法,实例构造器和父类方法四类。这些方法我们可以称为非虚方法(还有被final修饰的方法也是非虚方法),与之相对的其它方法我们称为虚方法。
- 这四类方法我们很好理解,静态方法,通过类调用不会产生歧义,私有方法,方法不会被类外调用,也不会产生歧义。构造器不会重名,不存在问题。还有父类方法,百度到唯一能让我信服的解释是,父类方法是指super关键字调用的方法,被super调用的方法即使在父类也是被重载的,也会选择最近的一个父类的方法进行调用,所以是可以确定版本,运行期不可变。如果理解有歧义望告知。
- 与之相对应的虚拟机提供了调用方法的字节码指令:
- invokestatic:调用静态方法;
- invokespecial:调用实例构造方法,私有方法和父类方法;
- invokevirtual:调用虚方法;
- invokeinterface:调用接口方法,在运行时再确定一个实现此接口的对象;
- invokedynamic:在运行时动态解析出调用点限定符所引用的方法之后,调用该方法;
- 很明显invokestatic和invokespecial指令它们调用的是非虚方法。不过还有一种非虚方法,就是被final修饰的方法也属于非虚方法,虽然它是由invokevirtual调用的。不过我不知道被final修饰的方法是不是通过解析转换为直接引用的,还没查到确定性的说明,好像可能似乎不是。
分派
- 解析调用是一个静态过程,在编译器就可以完全确认了,在加载时就可以装换为直接引用。而分派的调用可能是静态的,也可能是动态的,根据宗量数还可以分为多分派和单分派。另外虽然分为静态分派与动态分派,但他们都只是一个过程,并不互斥反而是互助的关系。
- 分派这个词听的很别扭,个人认为可以这样解释,分派就是符号引用依据一定宗量属性转化为具体方法版本(直接引用)。
静态分派
- 什么是静态分派?首先静态分派是在编译器就实现的,是为了在编译器就能确定调用方法的版本。在上学时我们学到过通过函数签名来确定一个方法的唯一性。简单来说就是方法名、方法参数个数以及各个参数的静态类型能确定类内唯一的方法。而实际调用方法时还需要加上调用方法所属对象的真实类型才能确定方法的版本。
- 静态分派是在编译器就实现的,那么在编译阶段我们可以确定什么?调用方法时所属对象的静态类型,方法名,调用时参数的个数及参数的静态类型,这些都是可以确定。那么除去调用参数的个数,方法名。则根据调用方法的对象的静态类型和参数的静态类型来确定调用方法的版本我们就称为静态分派。
动态分派
- 很显然,只根据静态分派并不一定能确定方法的真正版本,因为还有重写,方法可以被重写。在静态分派中只能确定调用方法的对象的静态类型,而在运行期才能确定调用方法的对象的实际类型,从而选择调用的方法真正的版本。而根据调用方法的对象的实际类型来确定调用方法的版本我们就称为动态分派。所以动态分派主要就是为了实现对方法重写的正确调用。
- 所以我们可以知道静态分派和动态分派是一个互助递进的关系,方法的符号引用(不是解析)必须得先在编译器经过静态分派,再在运行时通过动态分派才能确定方法的真实版本(直接引用)。
- 我个人学完后还有一个疑问,在编译阶段是否能知道运行期需要进行动态分派?如果能在编译器知道这个符号引用不需要执行动态分派,那么也还是需要在运行期才能将符号引用转化为直接引用的吗?
单分派与多分派
- 方法的调用者(接受者)和方法的参数统称为方法的宗量,根据分派基于多少宗量可以将分派分为单分派与多分派。单分派是根据一个方法对目标进行选择,多分派是根据多于一个宗量对方法进行选择。很明显静态分派是属于多分派的,它是根据调用者(静态类型)与方法的参数(静态类型)来确定方法版本的。而动态分派是属于单分派,它只是根据调用者(实际类型)来确定版本的。
- 所以java语言是一门静态多分派与动态单分派的语言。不过这说法也不正确,因为java语言在JSR-292中已经开始规划对动态语言的支持了。
虚拟机动态分派的实现
- 动态分派在运行时的操作非常佩服,而且在动态分派在选择方法版本时需要在调用对象类的元方法数据中搜索合适的目标方法,因此虚拟机在实际实现中基于性能的考虑,大部分都不会实现如此频繁的搜索。面对这种情况,最常见优化的手法是在方法区中建立一个虚方法表。如下图,虚方法表中会存放各个方法的实际入口地址,如果某个方法没有在子类中被重写,那么子类中这个虚方法表里这个虚方法指向的实际入口与父类中该虚方法表里指向的地址入口一致。如果子类重写了这个方法,那么子类虚方法表里的地址会指向子类重写的方法的地址入口。

- 为了在程序实现上方便,具有相同签名的方法在父类和子类的虚方法表里的索引号应该保持一致,这样当类型变换后,只需要变更查找的虚方法表,就可以根据索引号查找对应的虚方法地址了。
- 虚方法表的生成是在类加载的连接阶段进行该初始化的,准备类的变量初始值后,虚拟机也会把该类的虚方法表也初始化完毕。
- 除了类类型,java还有接口类型,与之对应的在方法区会生成接口方法表,使用的方法同虚方法表。
- 感觉写的很差,不知所云。。。。。。。哎
本文详细介绍了JVM中的方法调用过程,包括解析、静态分派、动态分派等概念。解析主要针对静态方法、私有方法、实例构造器和父类方法等非虚方法。静态分派在编译阶段完成,而动态分派则在运行时确定方法的真正版本。Java是静态多分派和动态单分派的语言,虚拟机通过虚方法表来优化动态分派的性能。
359

被折叠的 条评论
为什么被折叠?



