JVM方法调用

  1. 方法正常调用完成
  2. 方法调用非正常完成
  3. 方法返回地址
  4. 方法调用
  5. 解析

## 动态连接

每个栈帧内部都包含一个指向运行时常量池的引用来支持当前方法的代码实现动态连接.在Class文件中,描述一个方法调用其他方法,或者访问其他成员变量是通过符号引用来表示的.动态连接就是将这些符号引用所表示的方法转换为实际方法的直接引用.
类加载的过程中将要解析尚未被解析的符号引用, 并且将变量访问转换为访问这些变量的存储结构所在的运行时内存位置的正确偏移量.由于动态连接的存在,通过晚期绑定使用的其他类的方法和变量在发生变化时,将不会对调用他们的方法构成影响

每个栈帧都包含一个指向运行时常量池中该栈帧所属的方法引用,持有这个引用是为了支持调用过程中的动态连接.Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数. 这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析. 另外一部分将在每一次的运行期间转化为直接引用,这部分称为动态连接

方法正常调用完成

当前栈帧承担着恢复调用者状态的责任, 其状态包括调用这的局部变量表, 操作数栈以及被正确增加用来表示执行了该方法调用指令的程序计数器等。使得调用者的代码能在被调用的方法返回并且返回值被压入调用者栈帧的操作数栈后继续正常执行

方法调用非正常完成

指的是在方法调用过程了,某些指令导致了虚拟机抛出异常,而且虚拟机抛出的异常在该方法中没办法处理,或者在执行过程中遇到athrow字节码指令抛出的显式异常,同时在方法内部没有捕获异常

方法返回地址

当一个方法执行后,有俩个方式退出这个地址.第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者),是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式为正常完成出口.

另一种退出的方法是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论虚拟机内部产生的异常,还是代码中使用athrow字节码之类产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为异常完成出口.一个方法使用异常完成出口的方式退出,是不会给它的上层调用者产生任何返回值的.

无论采用何种退出方法,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态.一般来说,方法正常退出时,调用者的PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值.而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息.

方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:回复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以执行方法调用指令后面的一条指令等.

方法调用

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

解析

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

在java语言中,符合”编译器可知,运行期不可变”这个要求的方法主要是有静态方法和私有方法俩大类,前者与类型直接关联,后者在外部不可被访问,这俩种方法都不可能通过继承或别的方式重写出其他版本,因此他们都适合在类加载阶段进行解析.

与之对应的是,在java虚拟机里面提供了四条方法调用字节码指令:

  • invokestatic: 调用静态方法
  • invokespecial:调用实例构造器<init>方法,私有方法和父类方法
  • invokevirtual:调用所有的虚方法
  • invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象

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

1
2
3
4
5
6
7
8
9
10
11
public class StaticResolution {

public static void sayHello() {
System.out.println("hello");
}

public static void main(String[] args) {
StaticResolution.sayHello();
}
}

通过javap查看字节码:

1
2
3
4
5
6
7
8
9
10
11
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=1, args_size=1
0: invokestatic #5 // Method sayHello:()V
3: return
LineNumberTable:
line 9: 0
line 10: 3

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

解析调用一定是个静态过程,在编译期间就完全确定,在类装载的解析阶段就会把涉及的符号引用全部转变为可确定的直接引用,不会延迟到运行期再去完成.而分派调用则可能是静态的也可能是动态的,根据分派依据的宗数量可分为单分派和多分派.这俩类分派方式俩俩组合就构成了静态单分派,静态多分派,动态单分派,动态多分派.