前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >深入理解 JVM 之——字节码指令与执行引擎

深入理解 JVM 之——字节码指令与执行引擎

作者头像
浪漫主义狗
发布2023-09-07 10:29:01
发布2023-09-07 10:29:01
57600
代码可运行
举报
文章被收录于专栏:HAUE_LYS'BlogHAUE_LYS'Blog
运行总次数:0
代码可运行
  • 硬件依赖性:基于寄存器的指令集直接依赖硬件寄存器,因此在不同的硬件平台上可能存在差异,可移植性较差。
  • 编译器复杂性:基于寄存器的指令集需要考虑寄存器分配等复杂问题,编译器的实现相对较复杂。

基于栈的解释器执行过程


接下看我们具体看一个实际的代码示例:

代码语言:javascript
代码运行次数:0
运行
复制
public class Test {
    public static void main(String[] args) {
        int a = 114;
        int b = 514;
        int c = a + b;
    }
}

将上述代码保存为 Test.java 然后对其进行编译和反编译:

代码语言:javascript
代码运行次数:0
运行
复制
javac Test.java
javap -v Test.class

可以看到输出了如下内容:

代码语言:javascript
代码运行次数:0
运行
复制
Classfile /L:/JAVA/BasicSyntax/Learn_JVM/code/Test.class
  Last modified 2023年9月6日; size 276 bytes
  SHA-256 checksum 4064a19d96fe4d72c9d780ef819e1e937b120c31b37482e0b74c70e37c2a5601
  Compiled from "Test.java"
public class Test
  minor version: 0
  major version: 61
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #7                          // Test
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Class              #8             // Test
   #8 = Utf8               Test
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               main
  #12 = Utf8               ([Ljava/lang/String;)V
  #13 = Utf8               SourceFile
  #14 = Utf8               Test.java
{
  public Test();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: bipush        114
         2: istore_1
         3: sipush        514
         6: istore_2
         7: iload_1
         8: iload_2
         9: iadd
        10: istore_3
        11: return
      LineNumberTable:
        line 3: 0
        line 4: 3
        line 5: 7
        line 6: 11
}
SourceFile: "Test.java"

我们专注于下列信息:

代码语言:javascript
代码运行次数:0
运行
复制
public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: bipush        114
         2: istore_1
         3: sipush        514
         6: istore_2
         7: iload_1
         8: iload_2
         9: iadd
        10: istore_3
        11: return

其中:

  • public static void main(java.lang.String[]):表示这是一个公共的静态方法,方法名为 main,它接受一个 java.lang.String 类型的数组作为参数。
  • descriptor: ([Ljava/lang/String;)V:说明了方法的描述符,其中 ([Ljava/lang/String;) 表示参数类型为 java.lang.String 类型的数组,V 表示方法的返回类型为 void
  • flags: (0x0009) ACC_PUBLIC, ACC_STATIC:这是方法的标志,其中 ACC_PUBLIC 表示该方法是公共的,ACC_STATIC 表示该方法是静态的。

我们针对其中的 main 入口代码 Code 展示解释器的执行过程,其中:

代码语言:javascript
代码运行次数:0
运行
复制
stack=2, locals=4, args_size=1

提示我们这段代码需要深度为 2 的操作数栈、 4 个变量槽的局部变量空间和 1 个方法参数。

根据给定的字节码指令,我们可以模拟执行程序并跟踪操作数栈、局部变量表和程序计数器的动态变化过程。

首先,我们创建一个操作数栈(operand stack)和一个局部变量表(local variable table),并初始化程序计数器(program counter)为0。

代码语言:javascript
代码运行次数:0
运行
复制
执行:0: bipush        114
操作数栈状态:[114(栈顶), null]
局部变量表状态:[this(索引起始), null, null, null]
程序计数器状态:0
代码语言:javascript
代码运行次数:0
运行
复制
执行:2: istore_1
操作数栈状态:[null(栈顶), null]
局部变量表状态:[this, 114, null, null]
程序计数器状态:2
代码语言:javascript
代码运行次数:0
运行
复制
执行:3: sipush        514
操作数栈状态:[514(栈顶), null]
局部变量表状态:[this, 114, null, null]
程序计数器状态:3
代码语言:javascript
代码运行次数:0
运行
复制
执行:6: istore_2
操作数栈状态:[nul(栈顶), null]
局部变量表状态:[this, 114, 514, null]
程序计数器状态:6
代码语言:javascript
代码运行次数:0
运行
复制
执行:7: iload_1
操作数栈状态:[114(栈顶), null]
局部变量表状态:[this, 114, 514, null]
程序计数器状态:7
代码语言:javascript
代码运行次数:0
运行
复制
执行:8: iload_2
操作数栈状态:[114(栈顶), 514]
局部变量表状态:[this, 114, 514, null]
程序计数器状态:8
代码语言:javascript
代码运行次数:0
运行
复制
执行:9: iadd
操作数栈状态:[628(栈顶), null]
局部变量表状态:[this, 114, 514, null]
程序计数器状态:9
代码语言:javascript
代码运行次数:0
运行
复制
执行:10: istore_3
操作数栈状态:[null(栈顶), null]
局部变量表状态:[this, 114, 514, 628]
程序计数器状态:10
代码语言:javascript
代码运行次数:0
运行
复制
执行:11: return
操作数栈状态:[null(栈顶), null]
局部变量表状态:[this, 114, 514, 628]
程序计数器状态:11

上面的执行过程仅仅是一种概念模型,虚拟机最终会对执行过程做出一系列优化来提高性能,实际的运作过程并不会完全符合概念模型的描述。

更确切地说,实际情况会和上面描述的概念模型差距非常大,差距产生的根本原因是虚拟机中解析器和即时编译器都会对输入的字节码进行优化,即使解释器中也不是按照字节码指令去逐条执行的。

关于编译器优化的细节,我们会在以后的系列文章中提到。


方法调用指令


指令概述


针对调用不同类型的方法,字节码指令集里设计了不同的指令:

  1. invokestatic:用于调用静态方法。可以在类加载时将符号引用解析为直接引用。
  2. invokespecial:用于调用实例构造器 <init>() 方法、私有方法和父类中的方法。也可以在类加载时将符号引用解析为直接引用。
  3. invokevirtual:用于调用所有的虚方法。根据对象的实际类型进行分派(虚方法分派)。
  4. invokeinterface:用于调用接口方法,会在运行时确定实现该接口的对象,并选择适合的方法进行调用。
  5. invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法。分派逻辑由用户设定的引导方法决定。

这些调用指令可以根据对象的类型和方法的特性进行不同的分派和调用。

invokedynamic 指令是在 JDK 7时加入到字节码中的,当时确实只为了做动态语言(如 JRuby、Scala)支持,Java 语言本身并不会用到它。而到了JDK 8 时代,Java 有了 Lambda 表达式和接口的默认方法,它们在底层调用时就会用到 invokedynamic 指令。

其中,invokestaticinvokespecial 指令可以调用非虚方法,包括静态方法、私有方法、实例构造器和父类方法。而 invokevirtualinvokeinterface 指令用于调用虚方法,根据对象的实际类型进行分派。

也许这些指令看起来简单但很难理解,这是因为我们在上文多次提到过“方法调用”、“解析”、“分派”这些东西,别急,如果想要真正弄清楚这些指令,我们需要一步步来(

方法调用概述

方法调用并不等同于方法中的代码被执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还未涉及方法内部的具体运行过程。

一切方法调用在 Class 文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(直接引用)。

这个特性给 Java 带来了更强大的动态扩展能力,但也使得 Java 方法调用过程变得相对复杂,某些调用需要在类加载期间,甚至到运行期间才能确定目标方法的直接引用。

Javav 中,方法调用过程中同时存在解析(Resolution)和分派(Dispatch)两个过程,方法调用过程中首先进行解析,将符号引用转化为直接引用,然后根据实际对象的类型进行分派,确定方法的实际执行版本。

解析

解析:

  • 在类加载的解析阶段将方法调用的符号引用转化为直接引用的过程。

解析的前提:

  • 方法在程序编写、编译阶段就有一个可确定的调用版本,并且这个版本在运行期是不可改变的。
  • 必须是静态方法、私有方法和被 final 修饰的实例方法,因为它们都不可能通过继承或其他方式重写出其他版本。

解析调用过程:

  • 解析调用是静态的过程,在编译期间就完全确定,不延迟到运行期再完成。
  • 在类加载的解析阶段,涉及的符号引用会被转变为明确的直接引用,存储在常量池中。

这种转化使得方法调用在运行时可以更高效地执行,无需再进行符号解析,直接使用已经解析的直接引用。

分派(重点)

Java 作为一门面向对象的编程语言,具备继承、封装和多态这三个基本特征。

而分派调用过程在 Java 虚拟机中揭示了多态性的体现,特别是在方法的重载和重写方面:

  1. 重载(Overloading):重载是指在同一个类中定义多个方法,它们具有相同的名称但参数列表不同。在虚拟机中实现重载时,会根据方法调用的静态类型(声明类型)选择合适的方法版本。这属于静态分派,根据参数的静态类型来确定方法的版本。
  2. 重写(Overriding):重写是指子类重新定义父类中已有的方法,具有相同的名称和参数列表。在虚拟机中实现重写时,会根据方法调用的实际类型(运行时类型)选择合适的方法版本。这属于动态分派,根据实际对象的类型来确定方法的版本。

下面我们就来揭示 JVM 实现重载和重写的底层原理,这也是我们真正的重点部分。

静态分派

“分派”(Dispatch)这个词本身就具有动态性,一般不应用在静态语境之中,这部分原本在英文原版的《Java虚拟机规范》和《Java语言规范》里的说法都是“Method Overload Resolution”,即应该归入上节的“解析”里去讲解,但部分其他外文资料和国内翻译的许多中文资料都将这种行为称为“静态分派”。

为了解释静态分派和重载,我们看如下示例代码:

代码语言:javascript
代码运行次数:0
运行
复制
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);
    }
}

上面的代码中定义了一个 StaticDispatch 类,包含了一个抽象类 Human 和两个继承自 Human 的子类 ManWoman。类中定义了三个重载的 sayHello 方法,分别接受 HumanManWoman 类型的参数,并输出相应的问候语。

理论上我们重载了 sayHello() 方法,运行结果应该是:

代码语言:javascript
代码运行次数:0
运行
复制
hello,gentleman!
hello,lady!

但实际上控制台哼哼哼啊啊啊地输出了:

代码语言:javascript
代码运行次数:0
运行
复制
hello, guy!
hello, guy!

你先别急,让我先急 🤡

这里我们仍需要先提出几个概念:

  • 静态类型(Static Type):在编译时已知的变量类型,编译器根据静态类型进行方法的选择和类型检查。
  • 实际类型(Actual Type):在程序运行时确定的变量类型,由对象的实际创建类型决定。

在上面的代码中,Human 是静态类型(也叫外观类型),而 ManWoman 则是实际类型(也叫运行时类型)。

我们用 javap 查看反编译结果:

代码语言:javascript
代码运行次数:0
运行
复制
Compiled from "StaticDispatch.java"
public class StaticDispatch {
  public StaticDispatch();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void sayHello(StaticDispatch$Human);
    Code:
       0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #13                 // String hello,guy!
       5: invokevirtual #15                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return

  public void sayHello(StaticDispatch$Man);
    Code:
       0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #21                 // String hello,gentleman!
       5: invokevirtual #15                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return

  public void sayHello(StaticDispatch$Woman);
    Code:
       0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #23                 // String hello,lady!
       5: invokevirtual #15                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #25                 // class StaticDispatch$Man
       3: dup
       4: invokespecial #27                 // Method StaticDispatch$Man."<init>":()V
       7: astore_1
       8: new           #28                 // class StaticDispatch$Woman
      11: dup
      12: invokespecial #30                 // Method StaticDispatch$Woman."<init>":()V
      15: astore_2
      16: new           #31                 // class StaticDispatch
      19: dup
      20: invokespecial #33                 // Method "<init>":()V
      23: astore_3
      24: aload_3
      25: aload_1
      26: invokevirtual #34                 // Method sayHello:(LStaticDispatch$Human;)V
      29: aload_3
      30: aload_2
      31: invokevirtual #34                 // Method sayHello:(LStaticDispatch$Human;)V
      34: return
}

可以明显地看到 main 方法里面的 2631 是我们的方法调用:

代码语言:javascript
代码运行次数:0
运行
复制
26: invokevirtual #34                 // Method sayHello:(LStaticDispatch$Human;)V
......
31: invokevirtual #34                 // Method sayHello:(LStaticDispatch$Human;)V

反编译结果已经指明了,尽管 invokevirtual 可以根据对象的实际类型进行动态分派,但在静态分派的情况下,编译器已经确定了要调用的方法,因此不会进行动态分派,而是直接调用编译时选择的方法。

即,在静态分派的规则下,方法的选择是基于参数的静态类型,而不是实际运行时的类型

这样也就不难理解了,由于 manwoman 的静态类型都是 Human,所以会调用 HumansayHello() 方法。

但如果我们对原代码稍作修改:

代码语言:javascript
代码运行次数:0
运行
复制
public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        StaticDispatch sr = new StaticDispatch();
        sr.sayHello((Man) man);  
        sr.sayHello((Woman) woman);
}

再次编译运行,可以看到结果如下:

代码语言:javascript
代码运行次数:0
运行
复制
hello,gentleman!
hello,lady!

我们用 javap 查看反编译结果:

代码语言:javascript
代码运行次数:0
运行
复制
26: checkcast     #25                 // class StaticDispatch$Man
29: invokevirtual #34                 // Method sayHello:(LStaticDispatch$Man;)V
......
34: checkcast     #28                 // class StaticDispatch$Woman
37: invokevirtual #38                 // Method sayHello:(LStaticDispatch$Woman;)V

可以看到在调用方法之前,先进行了 checkcast 检查,确认了 manwoman 强制转换为了对应的实际类型,这样在 invokevirtual 指令进行方法调用时,指向的就是对应实际类型的 sayHello() 方法了。通过进行强制类型转换,即使在静态类型已经确定的情况下,我们仍绕过了静态分派的规则,使得方法的选择基于实际类型而不是静态类型。

动态分派

动态分派(Dynamic Dispatch)是一种在运行时根据对象的实际类型来选择调用的方法的机制,它与 Java 语言多态性的另外一个重要体现——重写(Override)有着很密切的关联。

我们将上节示例代码稍作修改:

代码语言:javascript
代码运行次数:0
运行
复制
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();
    }
}

在上述代码中,Human 是一个抽象类,其中声明了一个抽象方法 sayHello()ManWoman 类都是 Human 类的子类,它们分别重写了 sayHello() 方法。我们首先创建了 ManWoman 的实例对象 manwoman 并调用了相应的 sayHello() 方法,然后让 man 重新赋值为 Woman 的实例,并调用其 sayHello() 方法。

理论上运行结果应该为:

代码语言:javascript
代码运行次数:0
运行
复制
man say hello
woman say hello
woman say hello

实际上:

代码语言:javascript
代码运行次数:0
运行
复制
man say hello
woman say hello
woman say hello

这个运行结果相信不会出乎任何人的意料 🤗

对于习惯了面向对象思维的我们来说,这是一个理所应当的结果,但问题在于 Java 虚拟机是如何根据实际类型来分派方法执行版本的呢?

我们继续用 javap 大法查看反编译结果:

代码语言:javascript
代码运行次数:0
运行
复制
Compiled from "DynamicDispatch.java"
public class DynamicDispatch {
  public DynamicDispatch();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #7                  // class DynamicDispatch$Man
       3: dup
       4: invokespecial #9                  // Method DynamicDispatch$Man."<init>":()V
       7: astore_1
       8: new           #10                 // class DynamicDispatch$Woman
      11: dup
      12: invokespecial #12                 // Method DynamicDispatch$Woman."<init>":()V
      15: astore_2
      16: aload_1
      17: invokevirtual #13                 // Method DynamicDispatch$Human.sayHello:()V
      20: aload_2
      21: invokevirtual #13                 // Method DynamicDispatch$Human.sayHello:()V
      24: new           #10                 // class DynamicDispatch$Woman
      27: dup
      28: invokespecial #12                 // Method DynamicDispatch$Woman."<init>":()V
      31: astore_1
      32: aload_1
      33: invokevirtual #13                 // Method DynamicDispatch$Human.sayHello:()V
      36: return
}

main 方法中:

代码语言:javascript
代码运行次数:0
运行
复制
Human man = new Man();
Human woman = new Woman();

对应字节码指令为:

代码语言:javascript
代码运行次数:0
运行
复制
 0: new           #7                  // class DynamicDispatch$Man
 3: dup
 4: invokespecial #9                  // Method DynamicDispatch$Man."<init>":()V
 7: astore_1
 8: new           #10                 // class DynamicDispatch$Woman
11: dup
12: invokespecial #12                 // Method DynamicDispatch$Woman."<init>":()V
15: astore_2

而调用方法:

代码语言:javascript
代码运行次数:0
运行
复制
man.sayHello();
woman.sayHello();

对应字节码指令为:

代码语言:javascript
代码运行次数:0
运行
复制
17: invokevirtual #13                 // Method DynamicDispatch$Human.sayHello:()V
......
21: invokevirtual #13                 // Method DynamicDispatch$Human.sayHello:()V

可以看到 invokevirtual 指令注释已经显示了这个常量是 DynamicDispatch 类下 Human.sayHello()的符号引用,但是这两句指令最终执行的目标方法并不相同。

我们从 invokevirtual 指令本身入手,其运行时解析过程大致分为以下几步:

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

因此,invokevirtual 指令在执行时会先确定接收者的实际类型,所以两次调用中的 invokevirtual 指令并不是把常量池中方法的符号引用解析到直接引用上就结束了,还会根据方法接收者的实际类型来选择方法版本。

单分派与多分派

方法的接收者与方法的参数统称为方法的宗量,根据分派基于多少种宗量,可以将分派划分为单分派和多分派两种:

  1. 单分派指的是根据方法调用的接收者的类型来确定使用哪个方法实现。
    • 在单分派中,方法的选择仅仅依赖于接收者的类型,不考虑方法参数的类型。
  2. 多分派指的是根据方法调用的接收者和参数的类型来确定使用哪个方法实现。
    • 在多分派中,方法的选择不仅依赖于接收者的类型,还依赖于方法参数的类型。

对于 Java 来说:

  • 静态分派根据方法调用时的静态类型和参数的静态类型来选择目标方法,属于多分派。
  • 在动态分根据方法调用时的实际类型来选择目标方法,属于单分派。

invokedynamic

JDK 7 为了更好地支持动态类型语言,引入了第五条方法调用的字节码指令 invokedynamic,如果你看过我之前写过的浅谈 Java 中的 Lambda 表达式,其中在 Lambda 的本质一节我也提到了它,那么接下来我们好好康康到底怎么个事(

我们沿用其中的示例:

代码语言:javascript
代码运行次数:0
运行
复制
public class LambdaTest {

    public static interface Test {
        String showTestNumber(Integer param);
    }

    public static void main(String[] args) {
        Test test = param -> "Test number is " + param;
        System.out.println(test.showTestNumber(114514));
    }
}

javac 编译后再 javap 反编译回去得到下面的内容:

代码语言:javascript
代码运行次数:0
运行
复制
Classfile /L:/JAVA/BasicSyntax/Learn_JVM/code/LambdaTest.class
  Last modified 2023年9月6日; size 1445 bytes
  SHA-256 checksum 3a71d05fe531173bda2fd05e7b9a5a12dc0fd040047f87a59add471744a6a2be
  Compiled from "LambdaTest.java"
public class LambdaTest
  minor version: 0
  major version: 61
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #38                         // LambdaTest
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 3, attributes: 4
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = InvokeDynamic      #0:#8          // #0:showTestNumber:()LLambdaTest$Test;
   #8 = NameAndType        #9:#10         // showTestNumber:()LLambdaTest$Test;
   #9 = Utf8               showTestNumber
  #10 = Utf8               ()LLambdaTest$Test;
  #11 = Fieldref           #12.#13        // java/lang/System.out:Ljava/io/PrintStream;
  #12 = Class              #14            // java/lang/System
  #13 = NameAndType        #15:#16        // out:Ljava/io/PrintStream;
  #14 = Utf8               java/lang/System
  #15 = Utf8               out
  #16 = Utf8               Ljava/io/PrintStream;
  #17 = Integer            114514
  #18 = Methodref          #19.#20        // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
  #19 = Class              #21            // java/lang/Integer
  #20 = NameAndType        #22:#23        // valueOf:(I)Ljava/lang/Integer;
  #21 = Utf8               java/lang/Integer
  #22 = Utf8               valueOf
  #23 = Utf8               (I)Ljava/lang/Integer;
  #24 = InterfaceMethodref #25.#26        // LambdaTest$Test.showTestNumber:(Ljava/lang/Integer;)Ljava/lang/String;
  #25 = Class              #27            // LambdaTest$Test
  #26 = NameAndType        #9:#28         // showTestNumber:(Ljava/lang/Integer;)Ljava/lang/String;
  #27 = Utf8               LambdaTest$Test
  #28 = Utf8               (Ljava/lang/Integer;)Ljava/lang/String;
  #29 = Methodref          #30.#31        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #30 = Class              #32            // java/io/PrintStream
  #31 = NameAndType        #33:#34        // println:(Ljava/lang/String;)V
  #32 = Utf8               java/io/PrintStream
  #33 = Utf8               println
  #34 = Utf8               (Ljava/lang/String;)V
  #35 = InvokeDynamic      #1:#36         // #1:makeConcatWithConstants:(Ljava/lang/Integer;)Ljava/lang/String;
  #36 = NameAndType        #37:#28        // makeConcatWithConstants:(Ljava/lang/Integer;)Ljava/lang/String;
  #37 = Utf8               makeConcatWithConstants
  #38 = Class              #39            // LambdaTest
  #39 = Utf8               LambdaTest
  #40 = Utf8               Code
  #41 = Utf8               LineNumberTable
  #42 = Utf8               main
  #43 = Utf8               ([Ljava/lang/String;)V
  #44 = Utf8               lambda$main$0
  #45 = Utf8               SourceFile
  #46 = Utf8               LambdaTest.java
  #47 = Utf8               NestMembers
  #48 = Utf8               BootstrapMethods
  #49 = MethodHandle       6:#50          // REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
  #50 = Methodref          #51.#52        // java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
  #51 = Class              #53            // java/lang/invoke/LambdaMetafactory
  #52 = NameAndType        #54:#55        // metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
  #53 = Utf8               java/lang/invoke/LambdaMetafactory
  #54 = Utf8               metafactory
  #55 = Utf8               (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
  #56 = MethodType         #28            //  (Ljava/lang/Integer;)Ljava/lang/String;
  #57 = MethodHandle       6:#58          // REF_invokeStatic LambdaTest.lambda$main$0:(Ljava/lang/Integer;)Ljava/lang/String;
  #58 = Methodref          #38.#59        // LambdaTest.lambda$main$0:(Ljava/lang/Integer;)Ljava/lang/String;
  #59 = NameAndType        #44:#28        // lambda$main$0:(Ljava/lang/Integer;)Ljava/lang/String;
  #60 = MethodHandle       6:#61          // REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
  #61 = Methodref          #62.#63        // java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
  #62 = Class              #64            // java/lang/invoke/StringConcatFactory
  #63 = NameAndType        #37:#65        // makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
  #64 = Utf8               java/lang/invoke/StringConcatFactory
  #65 = Utf8               (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
  #66 = String             #67            // Test number is \u0001
  #67 = Utf8               Test number is \u0001
  #68 = Utf8               InnerClasses
  #69 = Utf8               Test
  #70 = Class              #71            // java/lang/invoke/MethodHandles$Lookup
  #71 = Utf8               java/lang/invoke/MethodHandles$Lookup
  #72 = Class              #73            // java/lang/invoke/MethodHandles
  #73 = Utf8               java/lang/invoke/MethodHandles
  #74 = Utf8               Lookup
{
  public LambdaTest();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=1
         0: invokedynamic #7,  0              // InvokeDynamic #0:showTestNumber:()LLambdaTest$Test;
         5: astore_1
         6: getstatic     #11                 // Field java/lang/System.out:Ljava/io/PrintStream;
         9: aload_1
        10: ldc           #17                 // int 114514
        12: invokestatic  #18                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        15: invokeinterface #24,  2           // InterfaceMethod LambdaTest$Test.showTestNumber:(Ljava/lang/Integer;)Ljava/lang/String;
        20: invokevirtual #29                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        23: return
      LineNumberTable:
        line 8: 0
        line 9: 6
        line 10: 23
}
SourceFile: "LambdaTest.java"
NestMembers:
  LambdaTest$Test
BootstrapMethods:
  0: #49 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #56 (Ljava/lang/Integer;)Ljava/lang/String;
      #57 REF_invokeStatic LambdaTest.lambda$main$0:(Ljava/lang/Integer;)Ljava/lang/String;
      #56 (Ljava/lang/Integer;)Ljava/lang/String;
  1: #60 REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #66 Test number is \u0001
InnerClasses:
  public static #69= #25 of #38;          // Test=class LambdaTest$Test of class LambdaTest
  public static final #74= #70 of #72;    // Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles

我们还是重点灌注 main 方法里面的内容:

代码语言:javascript
代码运行次数:0
运行
复制
public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=1
         0: invokedynamic #7,  0              // InvokeDynamic #0:showTestNumber:()LLambdaTest$Test;
         5: astore_1
         6: getstatic     #11                 // Field java/lang/System.out:Ljava/io/PrintStream;
         9: aload_1
        10: ldc           #17                 // int 114514
        12: invokestatic  #18                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        15: invokeinterface #24,  2           // InterfaceMethod LambdaTest$Test.showTestNumber:(Ljava/lang/Integer;)Ljava/lang/String;
        20: invokevirtual #29                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        23: return
      LineNumberTable:
        line 8: 0
        line 9: 6
        line 10: 23
}

其中:

代码语言:javascript
代码运行次数:0
运行
复制
0: invokedynamic #7,  0              // InvokeDynamic #0:showTestNumber:()LLambdaTest$Test;

就对应了我们的:

代码语言:javascript
代码运行次数:0
运行
复制
Test test = param -> "Test number is " + param;

下面我们来具体展示 invokedynamic 指令的运作过程:

  1. 0: invokedynamic #7, 0 提示我们需要找到常量池中索引为 #7InvokeDynamic 项。根据反编译结果,我们可以找到这个项的描述符为 #0:showTestNumber:()LLambdaTest$Test;,这也是 javap -v 预先添加的注释内容。
  2. 接下来 #7 后面紧跟的 0 是我们需要寻找的解析引导方法,即最后的 BootstrapMethods 中值为 0 的内容,其引导方法描述符指向了 LambdaMetafactory.metafactory 方法。
  3. 继续执行引导方法 #49:表示调用的静态方法 LambdaMetafactory.metafactory 接受六个参数并返回一个CallSite对象,参数如下:
    • java/lang/invoke/MethodHandles$Lookup:表示一个MethodHandles.Lookup对象,用于查找要调用的方法
    • java/lang/String:表示一个字符串,用于指定要实现的函数接口的名称。
    • java/lang/invoke/MethodType:表示一个方法类型对象,指定要实现的函数接口的方法签名。
    • java/lang/invoke/MethodType:表示一个方法类型对象,指定要实现的函数接口的方法签名。
    • java/lang/invoke/MethodHandle:表示一个方法句柄,指向要调用的方法。
    • java/lang/invoke/MethodType:表示一个方法类型对象,指定要调用的方法的方法签名。
  4. 成功返回后,将 CallSite 对象与目标方法进行绑定,该方法有三个参数:
    • #56:是一个方法类型(Ljava/lang/invoke/MethodType)的参数,表示被调用方法的参数类型和返回类型。
    • #57:是一个方法句柄(Ljava/lang/invoke/MethodHandle)的参数,表示要调用的方法的句柄。
    • #56:是一个方法类型(Ljava/lang/invoke/MethodType)的参数,表示被调用方法的参数类型和返回类型。
  5. 结合静态方法 LambdaMetafactory.metafactory 的第五个参数(java/lang/invoke/MethodHandle方法句柄)可知最终绑定到了 LambdaTest.lambdamain 0 方法,这也就是 Lambda 表达式最终在 LambdaTest 中的 main 方法里生成的私有方法。
  6. 最后,由于 invokedynamic 指令在一开始调用了showTestNumber方法,最终将返回的CallSite对象存储在局部变量表中等待调用。

通过这个过程,invokedynamic 指令实现了对 LambdaTest 类中 showTestNumber 方法的动态调用。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-9-06 1,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 类文件结构
    • Write Once,Run Anywhere
    • 类文件结构概述
  • 字节码指令
    • 生成反编译文件
    • 常见字节码指令
      • 加载和存储指令
      • 运算相关指令
      • 类型转换指令
      • 对象创建与访问指令
      • 操作数栈管理指令
      • 控制转移指令
      • 方法返回指令
    • 执行引擎概述
    • 运行时的栈帧结构
    • 字节码的解释执行
      • 基于栈的指令集和基于寄存器的指令集
      • 基于栈的解释器执行过程
  • 方法调用指令
    • 指令概述
    • 方法调用概述
    • 解析
    • 分派(重点)
      • 静态分派
      • 动态分派
      • 单分派与多分派
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档