在Java源文件编译为Class文件时,并不包含传统编译中的“连接”步骤。即一切方法调用在Class文件中都只是符号引用,而不是真正的指向方法在实际运行内存布局中的入口地址(相当于类加载过程中“解析”步骤中的直接引用)。因此才给Java带来强大的动态拓展能力,但也使Java的方法调用过程变得复杂,需要类在加载期间甚至在运行期间才能确定调用目标方法的直接引用。
方法调用过程包括以下几个步骤:
解析
因为所有的方法调用的目标方法在Class文件中都是一个常量池中的符号引用,在类加载的解析阶段,这些符号引用中的一部分会被翻译为直接引用。这种解析能够成立的前提是:方法在程序真正运行之前就有一个可确定的执行版本(也就是具体哪一个方法),并且这个方法在程序运行过程中不会被改变。换句话说,调用目标在程序代码写好、编译器进行编译时就已经能够确定下来。这类方法的调用称为“解析”。
Java语言中符合“编译期可知,运行时期不可变”要求的方法主要有静态方法和私有方法两大类,前者直接与类型关联,后者由于外部不可访问,所以这两种方法都不可能通过继承或者别的方式重写出其他版本,因此他们都适合在编译期进行解析。
Java虚拟机中提供了4条方法调用字节码指令(这里是jdk1.6标准)
- invokestatic:调用静态方法。
- invokespecial:调用实例构造器<init>方法、私有方法和父类方法。
- invokevirtual:调用所有虚方法。
- invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象。
只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段确定唯一版本,符合这个条件的有静态方法,私有方法,构造器和父类方法。它们在类加载的时候就会把符号引用解析为该方法的直接引用。这些方法也称作“非虚方法”,与之相反的方法(除了final修饰过的)被称为虚方法。
解析调用一定是一个静态过程,在编译期间就可以完全确定。
分派
静态分派
静态分派主要是牵涉到类的重载(Overload)。在说明重载问题之前,先按如下代码定义两个重要的概念:
Animal rabbit = new Rabbit();
在上述代码中,“Animal ”被称为变量的静态类型(Static Type)或者外观类型(Apparent Type),后面的“Rabbit”被称为变量的实际类型(Actual Type)。静态类型是编译期可知的,而实际类型在运行期才可以确定。使用哪个重载版本,完全取决于传入参量的数量和数据类型。编译器在重载时是通过参数的静态类型而不是实际类型作为判定依据。而由于静态类型是不会变化的,所以,在编译阶段就已经明确了要调用哪个方法版本。
静态分派选择的重载版本往往并不是唯一的,只能确定一个“更加合适”的版本。产生这种模糊结论的原因是因为字面量不需要定义,所以字面量没有显式的静态类型,它的静态类型只能通过语言上的规则去理解和推断。
动态分派
它是多态性的另一个重要体现——重写(Override)。在运行期根据变量的实际类型来确定方法执行的版本。虚拟机在实现动态分派时采用的方法是,在类的方法区中建立一个“虚方法表”。虚方法表中存放着各个方法的实际入口地址,如果某个方法在子类中没有被重写,那么子类的虚方法表里面的入口地址就和父类相同方法的地址入口是一致的。如果子类重写了父类方法,子类方法表中的地址会被替换为指向子类实现版本的入口地址。