方法调用的主要任务就是确定被调用方法的版本(即调用哪一个方法),该过程不涉及方法具体的运行过程。class文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在class文件中存储的都是符号引用,而不是方法在实际运行时内存布局中的入口地址,这使得java有着更强大的动态扩展能力,但也使得java方法的调用过程变得相对复杂起来,需要在类的加载甚至运行期间才能确定目标方法的直接引用。 按照调用方式共分为两类:
解析调用时是静态的过程,在编译期间就完全确定目标方法。分派调用则即可能是静态,也可能是动态的,根据分派标准可以分为单分派和多分派。两两组合有形成了静态单分派、静态多分派、动态单分派、动态多分派在Class文件中,所有方法调用中的目标方法都是常量池中的符号引用,在类加载的解析阶段,会将一部分符号引用转为直接引用,也就是在编译阶段就能够确定唯一的目标方法,这类方法的调用成为解析调用。此类方法主要包括静态方法和私有方法两大类,前者与类型直接关联,后者在外部不可访问,因此决定了他们都不可能通过继承或者别的方式重写该方法,符合这两类的方法主要有以下几种:静态方法、私有方法、实例构造器、父类方法。虚拟机中提供了以下几条方法调用指令:
invokestatic:调用静态方法,解析阶段确定唯一方法版本invokespecial:调用<init>方法、私有及父类方法,解析阶段确定唯一方法版本invokevirtual:调用所有虚方法invokeinterface:调用接口方法invokedynamic:动态解析出需要调用的方法,然后执行前四条指令固化在虚拟机内部,方法的调用执行不可认为干预,而invokedynamic指令则支持由用户确定方法版本。
其中invokestatic指令和invokespecial指令调用的方法称为非虚方法,其余的(final修饰的除外)称为虚方法。
虽然final方法是使用invokevirtual指令来调用,但是它无法被覆盖,没有其他版本,所以也无须对方法接收者进行多态选择,又说多态选择的结果肯定是唯一的。
依赖静态类型来定位方法执行版本的分派动作,称为静态分派。静态分派的最典型的应用就是方法重载。
静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。
在运行期间根据实际类型来确定方法执行版本的分派调用过程称为动态分派。这跟多态性的另一个体现——重写有着很密切的关联。
分别把刚刚创建的两个对象的引用压到栈顶,这两个对象是将要执行的方法的所有者,称为接收者;然后是是方法调用指令,这两条调用指令单从字节码角度来看,无论是指令(都是invokevirtual)还是参数完全一样的,但是这两句指令最终执行的目标方法并不相同。
原因就需要从invokevirtual指令的多态查找过程开始说起,invokevirtual指令的运行时解析过程大致分为以下几个步骤:
找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalaccessError异常。否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。由于invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,所以两次调用中的invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上,这个过程就是Java语言中方法重写的本质。我们把这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。
方法的接收者与方法的参数统称为方法的宗量。根据分派基于多少种宗量,可以将分派划分为单分派和多分派两种。单分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行选择。
Java语言的静态分派属于多分派类型。 Java语言的动态分派属于单分派类型。
由于动态分派是非常频繁的动作,而且动态分派的方法版本选择过程需要运行时在类的方法元数据中搜索合适的目标方法,因此在虚拟机的实际实现中基于性能的考虑,大部分实现都不会真正地进行如此频繁的搜索。面对这种情况,最常用的“稳定优化”手段就是为类在方法区中建立一个虚方法表(Vritual Method Table,也称为vtable,与此对应的,在invokeinterface执行时也会用到接口方法表——Inteface Method Table,简称itable),使用虚方法表索引来代替元数据查找以提高性能。
虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。如果子类中重写了这个方法,子类方法表中的地址将会替换为指向子类实现版本的入口地址。
为了程序实现上的方便,具有相同签名的方法,在父类、子类的虚方法表中都应当具有一样的索引序号,这样当类型变换时,仅需要变更查找的方法表,就可以从不同的虚方法表中按索引转换出所需的入口地址。方法表一般在类加载的连接阶段进行初始化,准备了类的变量初始值后,虚拟机会把该类的方法表也初始化完毕。
ps:方法表是分派调用的“稳定优化”手段,虚拟机除了使用方法表之外,在条件允许的情况下,还会使用内联缓存(Inline Cache)和基于“类型继承关系分析”(Class Hierarchy Analysis,CHA)技术的守护内联(Guarded Inlining)两种非稳定的“激进优化”手段来获得更高的性能,关于这两种优化技术的原理和运作过程,可以参考JIT晚期运行期。
新闻热点
疑难解答