java方法调用原理——虚拟机中方法调用

为了更加深入的理解方法的覆盖和覆写原理需要了解java方法的调用原理

首先解释一下方法调用:
方法调用不等同于方法执行,方法调用阶段的唯一任务就是确定被调用方法的版本(即确定具体调用那一个方法),不涉及方法内部具体运行。

java虚拟机中提供了5条方法调用的字节码指令:

invokestatic:调用静态方法
invokespecial:调用实例构造器方法、私有方法、父类方法
invokevirtual:调用所有虚方法。
invokeinterface:调用接口方法,在运行时再确定一个实现该接口的对象
invokedynamic:运行时动态解析出调用的方法,然后去执行该方法。

方法调编译成Class字节码后的状态:
Class文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在Class文件里存储的都只是符号引用,而不是具体的方法执行入口地址(即直接引用)。

所以对于java的方法调用过程就变的复杂起来,需要在类加载期间,甚至是运行期间才能确定目标方法的直接调用。

关于具体的类加载机制可以参考JVM类加载机制

1、解析阶段

Class文件里所有的方法调用都是一个常量池中的符号引用,在类的解析阶段,会将其中一部分符号引用转化为直接引用。转化的这部分方法调用必须是:在程序运行之前就有一个可以确定的调用版本,并且这个调用版本在程序运行期间是不可改变的。

即:编译期可知,运行期不变的方法调用

这种类型的方法主要有:静态方法、私有方法。

只要能被invokestatic和invokespecial指令调用的方法都可以在解析阶段中确定唯一调用版本。符合这个条件主要有:静态方法、私有方法、实例构造器方法、父类方法。他们在类加载的时候就会把符号引用解析为该方法的直接引用。

2、分派

就是如何确定执行哪个方法,这里详细解释了重载和重写。

静态分派

首先看一个重载的例子

public class Main2 {
    class A{
    }
    class B extends A{
    }
    public static void f(A a) {
        System.out.println("A");
    }
    public static void f(B b) {
        System.out.println("B");
    }
    public static void main(String[] args) {
        A a = new Main2().new B();
        f(a);
    }
}

输出:A

这里将A称为静态类型或者外观类型,B称为实际类型

编译器在运行前只知道一个对象的静态类型,并不知道对象的实际类型。

f方法经过了重载,有两个不同的参数,虚拟机方法调用时,他会直接使用静态类型进行匹配。也就是说:重载时是通过参数的静态类型而不是实际类型作为判定依据。并且静态类型是在编译期可知的,因此在编译阶段,javac编译器就可以根据参数类型确定具体使用哪个重载版本。

  • 所有依赖静态类型来定位方法执行版本的分派动作称为静态分派
  • 静态分派的典型应用就是方法重载
  • 静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的

动态分派

这个分派体现了重写

主要和invokevirtual方法调用字节码有关,运行过程如下:

  1. 找到操作数栈顶的第一个元素所指向的对象的实际类型,记为C;
  2. 在C中寻找与常量中的描述符合简单名都一致的方法,进行权限校验,如通过则返回这个方法的直接引用,查找结束;如果不通过,返回异常。
  3. 否则,按照继承关系从下往上依次对C的各个父类进行第2步搜索和验证。
  4. 如果始终没有找到合适的方法,则抛出异常。

虚拟机动态分派的实现

最常用的手段就是在方法区中建立一个虚方法表,如果一个方法在子类中没有被重写,那么子类的虚方法表里的地址入口就和父类对应方法地址入口一致。

java方法调用原理——虚拟机中方法调用_第1张图片

即:每个对象都建立一个如上图的方法虚方法表,表中列出每个对象的所有方法,包括继承的方法,如果重写了对应的方法,则对应的地址就是重写方法的地址,如果没有重写就是原来的方法地址。

你可能感兴趣的:(JVM)