方法调用不等同于方法中的代码被执行,方法调用阶段唯一的任务就是确定被调用方法的版本。

1、解析

方法调用的目标方法在Class文件里面是一个常量池中的符号引用,在类加载的解析阶段,会将部分符号引用转化为直接引用。这种解析有一个前提:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可变的。这类方法的调用被称为解析(Resolution)

符合“编译期可知,运行期不变”的方法:

  • 静态方法:与类型直接关联

  • 私有方法:外部不可被访问

这两种方法各自的特点都决定了它们都不可能通过继承或别的方式重写出其他版本;

1.1 方法调用字节指令

调用不同的方法,字节码指令集里设计了不同的指令。在Java虚拟机支持以下五条调用字节码指令,分别是:

  • invokestatic:调用静态方法。

  • invokespecial:调用实例构造器<init>()方法、私有方法和父类中的方法。

  • invokevirtual:调用所有的虚方法。

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

  • invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法。 前面4 条调用指令,分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻辑是由用户设定的引导方法来决定的。

1.2 唯一确定版本的方法

只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,主要有

  • 静态方法

  • 私有方法

  • 实例构造器

  • 父类方法

  • final修饰的方法(它使用invokevirtual)

1.3 示例

静态方法sayHello(),只能属于类型StaticResolution,没有任何途径可以隐藏或覆盖这个方法

/**
* Created with IntelliJ IDEA.
*
* @Author: qiao
* @Date: 2022/11/12/16:55
* @Description: 方法静态解析演示
*/
public class StaticResolution {
    public static void sayHello() {
        System.out.println("hello world");
    }
    public static void main(String[] args) {
        StaticResolution.sayHello();
    }
}

使用javap命令查看对应字节码,可以看到是通过invokestatic命令来调用sayHello()方法的。

 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 15: 0
        line 16: 3
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       4     0  args   [Ljava/lang/String;
    MethodParameters:
      Name                           Flags
      args

2、分派

2.1 静态分派

为了理解静态分派,还是先从示例讲起