今天看啥  ›  专栏  ›  gzss

java方法调用-解析

gzss  · 简书  ·  · 2019-07-01 22:23

方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。在程序运行时,进行方法调用是最普遍和最频繁的操作,但是class文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(相当于直接引用)。这个特性给Java带来了强大的扩展能力,也使得Java方法调用过程变得相对复杂起来,需要在类家在期间,甚至到运行期间才能确定目标方法的直接引用。

解析

所有方法调用中的目标方法在class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这种解析能成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。换句话说,调用目标在程序代码写好、编译器进行编译时就必须确定下来。这类方法调用被称为解析(Resolution)。

在Java语言中符合“编译期可知,运行期不可变”这个要求的方法,主要包括静态方法和私有方法两大类,前者与类型直接相关,后者在外部不可被访问,这两种方法各自的特点决定了他们都不可能通过继承或者别的方式重写其他版本,因此他们都适合在类加载阶段进行解析。

与之相对应的,在java虚拟机里面提供了5条方法调用字节码指令,分别如下:

1、invokestatic:调用静态方法

2、invokespecial:调用实例构造器<init>方法、私有方法和父类方法。

3、invokevirtual:调用所有的虚方法。

4、invokeinterface:调用接口方法,会在运行时在确定一个实现此接口的对象。

5、invokedynamic:现在运行时动态解析出调用点限定符号所引用的方法,然后在执行该方法,在此之前的4条调用指令,分派逻辑是固化在java虚拟机内部的,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。

只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,符合这个条件的有静态方法、私有方法、实例构造器、父类方法4种,他们在类加载的时候就会把符号引用解析为该方法的直接引用。这些方法可称为非虚方法,与之相反,其他方法称为虚方法(出去final方法,后面会聊)。参见如下代码,演示了一个最常见的解析调用的例子,此样例中,静态方法sayHello()只可能属于类型StaticResolution,没有任何手段可以覆盖或隐藏这个方法。


使用javap命令查看这段程序的字节码,会发现是通过invokestatic命令来调用sayHello方法的,如下图。


java中的非虚方法除了使用invokestatic、invokespecial调用的方法之外还有一种,就是被final修饰的方法。虽然final修饰的方法使用invokevirtual指令来调用,但是由于它无法被覆盖,没有其他版本,所以也无需对方法接收者进行多态选择,又或者说多态选择的结果肯定是唯一的。在Java语言规范中明确说明了final方法是一种非虚方法。

解析调用是一个静态过程,在编译期间就完全确定,在类装载的解析阶段就会把涉及到的符号引用全部转变为可确定的直接引用,不会延迟到运行期才完成。而分派(dispatch)调用则可能是静态的也可能是动态的,根据分派的宗量数可分为单分派和多分派。这两类分派方式的两两组合就构成了静态单分派、静态多分派、动态单分派、动态多分派4种分派组合情况,后面文章会介绍虚拟机中的方法分派是如何进行的。




原文地址:访问原文地址
快照地址: 访问文章快照