方法调用:
方法调用不同于方法执行,方法调用阶段唯一任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不执行方法内部的具体过程。方法调用有,解析调用,分派调用(有静态分派,动态分派)。
方法解析:
解析调用一定是一个静态的过程,在编译期就完全确定,可以在类加载的解析阶段就把涉及的符号引用转化为直接引用,不会延迟到运行期再去完成。
所有方法调用的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中一部分符号引用转化为直接引用(前提是,方法在程序真正运行前就可以确定调用的版本,并且这个方法的调用版本在运行期不会改变,也就是说,调用目标在程序代码写好,编译器编译时就能确定下来)。这类方法的调用称为解析。
编译期可知,运行期不变,这样的方法主要有静态方法和私有方法这两大类,前者与类型关联,后者在外部不可被访问,这两种方法各自的特点决定了它们都不能通过继承或别的方式重写其他版本,因此它们都适合在类加载阶段进行解析。
与之对应的是,在Java虚拟机里面提供了5条调用字节码指令,有:
invokestatic:调用静态方法
invokespecial:调用实例构造器<init>方法,私有方法和父类方法。
invokevirtual:调用所有的虚方法
invokeinterface:调用接口方法,会在运行时在确定一个实现此接口的对象。
只要能被invodestatic,invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本(而invokevirtual,invokeinterface指令调用的方法则不行,因为他们2调用的方法需要去找到子类对应的方法(如果有的话),实现该接口的类的方法,这些都是在运行期确定的)。能被invokestatic,invokespecial指令对应的方法有静态方法,私有方法,实例构造器方法,父类方法,它们会在类加载的时候(准确说是在解析阶段)就会把符号引用转化为直接引用,这些方法称为非虚方法,与之对应的称为虚方法(final方法除外,即使它是虚方法,但是因为它不能被重写,可以在类加载的解析阶段就把涉及的符号引用转化为直接引用,不会延迟到运行期再去完成)。
分派之静态分派:
分派调用可能是静态的,也可能是动态的,根据分派的宗量数可分为单分派和多分派。分派调用过程将会揭示多态性特征的一些基本的体现,如重载,重写是怎样实现的。、
变量的类型有静态类型和实际类型之分,所以在加载器无法确定变量的实际类型,也就无法确定变量所调用方法的版本。
静态类型(或者称外观类型,C++中称为编译器类型),实际类型(C++中称为运行期类型)。这两个概念是实现多态的基础。
虚拟机(准确说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据的,静态类型是编译期可知的,因为在编译阶段,Javac编译器会根据参数的静态类型决定使用哪个重载版本。所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。静态分派的典型应用是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。另外,编译器虽然能确定出方法的重载版本,但在很多情况在这个版本有多个选择,往往只能确定一个更加合适的。如,基础类型变量,以字符变量为例重载时首选其实际的类型(接触类型没有静态类型这一说),然后是int,long,Character,Serializable,Object.
分派之动态分派:
invokevirtual指令(执行所有的虚方法)的运行时解析过程大致分为以下几个步骤:
1.找到操作数栈顶的第一个元素所指向的对象(调用当前方法的对象)的实际类型,记作C
2.如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过抛出java.lang.IllegalAccessError异常;
3.否则,按照继承关系从下往上依次对C的父类进行第2步的搜索和校验过程;
4.如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。
这个过程就是Java语言中方法重写的本质,在这个过程中将常量池中的类方法的符号引用转化为直接引用(针对于这些虚方法)。我们把这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。
单分派与多分派:
方法的接收者与方法的参数统称为方法的宗量。单分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行选择。
对照实例来分析:
1 public class Dispath 2 { 3 static class QQ{} 4 5 static class _360{} 6 7 public static class Father { 8 public void hardChoice(QQ arg){ 9 System.out.println("father choose qq"); 10 } 11 12 public void hardChoice(_360 arg){ 13 System.out.println("father choose 360"); 14 } 15 } 16 17 public static class Son extends Father { 18 public void hardChoice(QQ arg){ 19 System.out.println("son choose qq"); 20 } 21 22 public void hardChoice(_360 arg){ 23 System.out.println("son choose 360"); 24 } 25 } 26 27 public static void main(String[] args){ 28 Father father = new Father(); 29 Father son = new Son(); 30 father.hardChoice(new _360()); 31 son.hardChoice(new QQ()); 32 } 33 }
对于hardChoice()方法
1.编译器选择方法版本的过程,也就是静态分派的过程。这时选择目标方法的依据有两点:一点是静态类型是Father还是Son,二是方法参数是QQ还是360。这次选择结果的产物是产生了两条invokevirtual指令,两个指令的参数分别为常量池中指向Father.hardChoice(360)及Father.hardChoice(QQ)方法的符号引用。如图:
因为是根据两个宗量进行选择,所以Java语言的静态分派属于多分派类型。
2.运行阶段的选择,也就是动态分派的过程。在执行“son.hardChoice(new QQ())”这句代码时,即invokevirtual指令时,由于编译期已经决定目标方法的签名必须是hardChoice(QQ),虚拟机此时不会关心传进来的参数类型了,因为这时的参数的静态类型,实际类型已经对方法的选择不会产生影响了(已经进行了重载,剩下的只有重写了)。唯一产生影响的是方法调用者的实际类型是Father还是Son。因为只有一个宗量作为选择依据,所以动态分派属于单分派类型。
基于栈的字节码解释执行引擎:
许多Java虚拟机的执行引擎在执行Java代码时都有解释执行(通过解释器执行),编译执行(通过即时编译器产生本地代码执行)两种选择。
Java编译器输出的指令流,基本上是基于栈的指令集架构,指令流中的指令大部分是零地址指令,他们依赖操作栈进行工作。与之对应的是基于寄存器的指令集,如x86二地址指令集(主流PC机使用)。
基于栈的指令集优点:可移植,代码想对紧凑(一字节对应一个指令),编译器实现更加简单(不用考虑空间分配问题,所需空间在栈上操作)。
缺点:速度慢(因为指令数量多,内存访问频繁,访问栈属于内存访问)。
基于栈的解释器执行过程跟处理器执行方式相似。