「JVM 执行引擎」方法调用原理

方法调用 阶段唯一的任务是找到被调用的方法(而非方法的代码被执行);在 Class 文件存储的都是符号引用,而非在实际运行时内存布局中的入口地址(直接引用),这使得 Java 拥有更强大的动态扩展能力,但这也造成直到类加载甚至运行期间才知道调用目标方法的直接引用;

1. 解析

调用目标在程序写好、编译器编译时就已经确定(编译器可知,运行期不可变),这部分方法调用在类加载的解析阶段会将符号引用转化为方法的直接引用,而这样的方法调用被称为解析Resolution);

方法调用字节码指令

  • invokestatic,用于调用静态方法;
  • invokespecial,用于调用示例构造器 <init>() 方法、私有方法、父类中方法;
  • invokevirtual,用于调用所有的虚方法
  • invokeinterface,用于调用接口方法,在运行时再确定一个实现该接口的对象;
  • invokedynamic,在运行时动态解析出调用点限定符所引用的方法,在执行该方法;

前 4 种调用指令的分派逻辑固化在 JVM 内部,而 invokedynamic 指令的分派逻辑有用户设定的引导方法来决定;

  • 非虚方法Non-Virtual Method),使用 invokestatic 和 invokespecial 指令调用的方法都可以在解析阶段确认唯一方法版本(静态方法、私有方法、实例构造器、父类方法),invokevirtual 指令调用的方法若是 final 修饰的,也符合 编译器可知,运行期不可变 条件,这些方法可统一称为非虚方法,方法调用都会在类加载阶段将符号引用解析为方法的直接引用;此外的所有方法都可称为虚方法Virtual Method);

方法静态解析

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

    public static void main(String[] args) {
        StaticResolution.sayHello();
    }
}
// javap -verbose StaticResolution
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 13: 0
    line 14: 3
    LocalVariableTable:
    Start  Length  Slot  Name   Signature
        0       4     0  args   [Ljava/lang/String;
MethodParameters:
    Name                           Flags
    args

通过 invokestatic 命令调用了 sayHello() 方法,方法调用目标在编译时明确,即 invokestatic 指令参数指向的常量池项;

2. 分派

相比解析调用在编译期间完全确定调用目标,并在类加载阶段将涉及的符号引用全部转化为明确的直接引用这种静态方式,分派Dispatch)可以是静态,也可以是动态,可以是单分派,也可以是多分派

2.1. 静态分配

也称 Method Overload Resolution(即将静态分派当做解析);主要针对方法重载(Overload),在编译期确定、在类加载期静态分配重载方法的版本;

方法静态分派

public class StaticDispatch {
    static abstract class Human {
    }

    static class Man extends Human {
    }

    static class Woman extends Human {
    }

    public void sayHello(Human guy) {
        System.out.println("hello,guy!");
    }

    public void sayHello(Man guy) {
        System.out.println("hello,gentleman!");
    }

    public void sayHello(Woman guy) {
        System.out.println("hello,lady!");
    }

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        StaticDispatch sr = new StaticDispatch();
        sr.sayHello(man);
        sr.sayHello(woman);
    }
}

执行结果

hello,guy!
hello,guy!

JVM 选择重载版本取决于入参的数量和数据类型(静态类型,而非实际类型),此处两个调用的参数的静态类型皆为 Human,因此执行 sayHello(Human)

  • 静态类型Static Type),也叫外观类型Apparent Type),即变量的持有者类型,如上文 man 和 woman 的静态类型是 Human,可以通过强转改变静态类型,在编译期可知;
  • 实际类型Actual Type),也叫运行时类型Runtime Type),即变量实例化使用的真实类型,如上文 man 的实际类型 Man,woman 的实际类型 Woman,必须在程序运行到具体位置才能确定具体类型;
// 实际类型变化
Human human = (new Random()).nextBoolean() ? new Man() : new Woman();

// 静态类型变化
sr.sayHello((Man) human);
sr.sayHello((Woman) human);

字面量没有显示的静态类型,在入参是字面量的重载方法调用场景下,很多时候版本选择不是唯一的,只能确定一个相对更合适的版本

public class Overload {
    public static void sayHello(Object arg) {
        System.out.println("hello Object");
    }

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

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

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

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

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

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

    public static void main(String[] args) {
        sayHello('a');
    }
}

执行结果

hello char

// 注释掉 sayHello(char),'a' 发生一次自动类型转换(char -> int)
hello int

// 注释掉 sayHello(int),'a' 发生两次自动类型转换(char -> int -> long)
hello long

// 注释掉 sayHello(long),'a' 发生一次自动装箱(char -> Charactor)
hello Character

// 注释掉 sayHello(Character),'a' 发生一次自动装箱和一次自动类型装换(char -> Character -> Serializable)
hello Serializable

// 注释掉 sayHello(Serializable),'a' 发生一次自动装箱后,沿着继承关系往上搜索,越往上层优先级越低,对入参 null 依旧有效
hello Object

// 注释掉 sayHello(Object),所有重载方法中,可变长参数的重载优先级最低;
hello char ...

若同时存在两个父类的重载方法,编译将无法通过,提示类型模糊Type Ambiguous);

2.2. 动态分派

主要针对方法重写Override),根据方法接收者Receiver)的实际类型来选择重写方法的版本;

方法动态分派

public class DynamicDispatch {

    static abstract class Human {
        protected abstract void sayHello();
    }

    static class Man extends Human {
        @Override
        protected void sayHello() {
            System.out.println("man say hello");
        }
    }

    static class Woman extends Human {
        @Override
        protected void sayHello() {
            System.out.println("woman say hello");
        }
    }

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        man.sayHello();
        woman.sayHello();
        man = new Woman();
        man.sayHello();
    }
}

执行结果

man say hello
woman say hello
woman say hello

字节码分析

// javap -verbose DynamicDispatch
 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #2                  // class edu/aurelius/jvm/clazz/DynamicDispatch$Man
         3: dup
         4: invokespecial #3                  // Method edu/aurelius/jvm/clazz/DynamicDispatch$Man."<init>":()V
         7: astore_1
         8: new           #4                  // class edu/aurelius/jvm/clazz/DynamicDispatch$Woman
        11: dup
        12: invokespecial #5                  // Method edu/aurelius/jvm/clazz/DynamicDispatch$Woman."<init>":()V
        15: astore_2
        16: aload_1
        17: invokevirtual #6                  // Method edu/aurelius/jvm/clazz/DynamicDispatch$Human.sayHello:()V
        20: aload_2
        21: invokevirtual #6                  // Method edu/aurelius/jvm/clazz/DynamicDispatch$Human.sayHello:()V
        24: new           #4                  // class edu/aurelius/jvm/clazz/DynamicDispatch$Woman
        27: dup
        28: invokespecial #5                  // Method edu/aurelius/jvm/clazz/DynamicDispatch$Woman."<init>":()V
        31: astore_1
        32: aload_1
        33: invokevirtual #6                  // Method edu/aurelius/jvm/clazz/DynamicDispatch$Human.sayHello:()V
        36: return
      LineNumberTable:
        line 28: 0
        line 29: 8
        line 30: 16
        line 31: 20
        line 32: 24
        line 33: 32
        line 34: 36
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      37     0  args   [Ljava/lang/String;
            8      29     1   man   Ledu/aurelius/jvm/clazz/DynamicDispatch$Human;
           16      21     2 woman   Ledu/aurelius/jvm/clazz/DynamicDispatch$Human;
    MethodParameters:
      Name                           Flags
      args

在 17 和 21 行方法调用指令 invokevirtual接收者Receiver)分别是处在栈顶的 Man 对象和 Woman 对象,参数则都是 Human.sayHello
根据《Java 虚拟机规范》,invokevirtual 指令的解析过程如下:

  • 找到操作数栈顶第一个元素所指向的实际类型,计作 C;
  • 若类型 C 中找到与常量中描述符和简单名称相符的方法,则进行权限校验,成功则返回该方法的直接引用,结束查找;否则返回 java.lang.IllegalAccessError
  • 否则,按照继承关系从下往上依次对 C 的父类进行上一步搜索和验证;
  • 若始终无法找到,则抛出 java.lang.AbstractMethodError

字段不参与多态

public class FieldHasNoPolymorphic {
    static class Father {
        public int money = 1;

        public Father() {
            money = 2;
            showMeTheMoney();
        }

        public void showMeTheMoney() {
            System.out.println("I am Father, i have $" + money);
        }
    }

    static class Son extends Father {
        public int money = 3;

        public Son() {
            money = 4;
            showMeTheMoney();
        }

        public void showMeTheMoney() {
            System.out.println("I am Son, i have $" + money);
        }
    }

    public static void main(String[] args) {
        Father gay = new Son();
        System.out.println("This gay has $" + gay.money);
    }
}

执行结果

I am Son, i have $0
I am Son, i have $4
This gay has $2

创建 Son 类时,会先隐式调用 Father 的构造函数,而 Father 的构造函数中 showMeTheMoney() 是一个虚方法调用,实际接收者是 Son 的对象,因此调用的是 Son::showMeTheMoney(),而此时 Son 类的 money 字段值为 0;

子类的内存中两个字段都会存在,但子类的字段会屏蔽父类的同名字段,哪个类的方法访问字段,就返回哪个类的字段值(通过静态类型访问字段);

2.3. 单分派与多分派

方法的接收者与方法的参数统称为方法的宗量,根据一个宗量对目标方法进行选择叫单分派,根据多个宗量对目标方法进行选择是为多分派;

单分派、多分派

public class Dispatch {
    static class QQ {
    }

    static class _360 {
    }

    public static class Father {
        public void hardChoice(QQ arg) {
            System.out.println("father choose qq");
        }

        public void hardChoice(_360 arg) {
            System.out.println("father choose 360");
        }
    }

    public static class Son extends Father {
        public void hardChoice(QQ arg) {
            System.out.println("son choose qq");
        }

        public void hardChoice(_360 arg) {
            System.out.println("son choose 360");
        }
    }

    public static void main(String[] args) {
        Father father = new Father();
        Father son = new Son();
        father.hardChoice(new _360());
        son.hardChoice(new QQ());
    }
}

执行结果

father choose 360
son choose qq

如今的 Java 语言是一门静态多分派、动态但分派的语言;

  • 在编译阶段,即静态分派过程中,选择目标方法的依据是:静态类型是 Father 还是 Son(invokevirtual 指令的参数是静态类型的符号引用),参数是 QQ 还是 360;
  • 在运行阶段,即动态分派过程中,选择目标方法的依据是:接收者的实际类型是 Father 还是 Son(invokevirtual 指令的接收者类型);
2.4. 动态分派实现

动态分派的方法选择需要运行时在接收者类型的方法元数据中搜索合适的目标方法;出于性能考虑会在类型的方法区建立一个虚方法表(Virtual Method Table,vtable,存放了各个方法的实际入口地址;相应的 invokeinterface 会用接口方法表,Interface Method Table,itable)提高查找性能;

请添加图片描述

虚方法表在类加载的连接阶段进行初始化,类变量初始化后即会初始化类的虚方法表;

除了使用虚方法表(只在解释执行状态使用,实际是最慢的一种分派)提高方法查找性能,JVM 还使用了类型继承关系分析Class Hierarchy AnalysisCHA)、守护内联Guarded Inlining)、内联缓存Inline Cache)等多种非稳定的激进优化(在即时编译执行状态)来争取更大性能空间;


上一篇:「JVM 执行引擎」 虚拟机栈的栈帧结构
下一篇:「JVM 执行引擎」动态类型语言支持

PS:感谢每一位志同道合者的阅读,欢迎关注、评论、赞!


参考资料:

  • [1]《深入理解 Java 虚拟机》
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Aurelius-Shu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值