当前位置: 首页 > news >正文

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

更好的阅读体验 \huge{\color{red}{更好的阅读体验}} 更好的阅读体验

类文件结构


Write Once,Run Anywhere


对于 C 语言从程序到运行需要经过编译的过程,只有经历了编译后,我们所编写的代码才能够翻译为机器可以直接运行的二进制代码,并且在不同的操作系统下,我们的代码都需要进行一次编译之后才能运行。

Java 不同于 C,由于 JVM 内置了解释器和即时编译器,这使得 .java 可以不经过编译就直接通过解释的方式直接运行。关于解释器和即时编译器的内容我们将在以后介绍。

我们来看一段简单的代码:

public class Hello {public static void main(String[] args) {System.out.println("Hello world");}
}

我们将其保存为 Hello.java,然后运行:

java Hello.java

可以看到不出意外地输出了:

image-20230905163020567

这样我们就通过解释的方式运行了刚刚编写的 .java 文件,但为了更好的理解 Java 的类文件结构,我们接下来对 .java 文件进行编译:

javac Hello.java

可以看到在当前目录下生成了一个 Hello.class 文件:

image-20230905163351158

接下来我们运行:

java Hello

可以看到不出意外的输出了:

image-20230905163722881

这样我们就以运行编译后的文件方式运行了 .class 文件。

如果我们以十六进制打开该文件:

image-20230905163517473

你会看到上述文件充满了以两个字节为一组的字节码,前两组四个字节被成为“魔数”,说明该文件是否为一个能被虚拟机接受的 Class 文件,而 JavaClass 文件的魔数值为 0xCAFEBABE ☕👶。

“一次编写,到处运行”——这是 Java 在刚刚诞生之时提出的非常著名的宣传口号,而 Java 的解决方案便是将像上面这样的源代码编译为平台无关的中间格式——字节码文件(.class 文件),并通过对应的 Java 虚拟机读取和运行这些中间格式的编译文件,这就使得语言本身实现了跨平台。


类文件结构概述


Java 源代码经过编译器编译后会生成类似上面的字节码文件(.class 文件),其中字节码文件的结构如下:

  1. 魔数(Magic Number):字节码文件的前四个字节是一个固定的魔数(0xCAFEBABE),用于标识该文件为 Java 字节码文件。

  2. 版本信息(Version):紧随魔数之后的两个字节表示字节码文件的版本信息,分别是主版本号和次版本号。

  3. 常量池(Constant Pool):紧随版本信息之后是一个常量池表(Constant Pool Table),用于存储编译时生成的各种常量、符号引用和字面量。常量池中的每个项目都有一个索引,通过索引可以在运行时查找和使用。

  4. 访问标志(Access Flags):紧随常量池之后的两个字节表示类或接口的访问标志,用于指示该类或接口的访问级别和特性。

  5. 类索引、父类索引和接口索引(Class Indexes):紧随访问标志之后的两个字节表示类索引,用于指向常量池中该类的全限定名。接着是两个字节的父类索引,用于指向常量池中父类的全限定名。接口索引表紧随父类索引之后,用于指向常量池中实现的接口的全限定名。

  6. 字段表(Field Table):紧随接口索引之后是字段表,用于描述类或接口中定义的字段的访问标志、字段名、字段类型等信息。

  7. 方法表(Method Table):紧随字段表之后是方法表,用于描述类或接口中定义的方法的访问标志、方法名、方法参数、方法返回类型等信息。

  8. 属性表(Attribute Table):紧随方法表之后是属性表,用于存储与类、字段或方法相关的附加信息,如注解、代码行号表、异常表等。


字节码指令


生成反编译文件


由于字节码实在不是人读的难以理解,如果你感兴趣全部读懂可以自行深入学习( 真的有人用纯字节码编程吗,我们还是康康能理解一点的东西罢)

我们利用 javap 命令对 .class 文件反编译,将其输出到文本:

javap -v Hello.class > Hello.txt

javap 命令的一些常用选项包括:

  • -c:显示字节码指令。
  • -l:显示行号和本地变量表。
  • -s:显示内部类型签名。
  • -verbose:显示所有信息,可简化为 -v

然后打开 Hello.txt 查看:

Classfile /L:/JAVA/BasicSyntax/Learn_JVM/code/Hello.classLast modified 202395; size 415 bytesSHA-256 checksum 35b1e377d78c81fc0a324af427c3e67c3a468b293c544de8715343f4d97c0c52Compiled from "Hello.java"
public class Hellominor version: 0major version: 61flags: (0x0021) ACC_PUBLIC, ACC_SUPERthis_class: #21                         // Hellosuper_class: #2                         // java/lang/Objectinterfaces: 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 = Fieldref           #8.#9          // java/lang/System.out:Ljava/io/PrintStream;#8 = Class              #10            // java/lang/System#9 = NameAndType        #11:#12        // out:Ljava/io/PrintStream;#10 = Utf8               java/lang/System#11 = Utf8               out#12 = Utf8               Ljava/io/PrintStream;#13 = String             #14            // Hello world#14 = Utf8               Hello world#15 = Methodref          #16.#17        // java/io/PrintStream.println:(Ljava/lang/String;)V#16 = Class              #18            // java/io/PrintStream#17 = NameAndType        #19:#20        // println:(Ljava/lang/String;)V#18 = Utf8               java/io/PrintStream#19 = Utf8               println#20 = Utf8               (Ljava/lang/String;)V#21 = Class              #22            // Hello#22 = Utf8               Hello#23 = Utf8               Code#24 = Utf8               LineNumberTable#25 = Utf8               main#26 = Utf8               ([Ljava/lang/String;)V#27 = Utf8               SourceFile#28 = Utf8               Hello.java
{public Hello();descriptor: ()Vflags: (0x0001) ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 1: 0public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: (0x0009) ACC_PUBLIC, ACC_STATICCode:stack=2, locals=1, args_size=10: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc           #13                 // String Hello world5: invokevirtual #15                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: returnLineNumberTable:line 3: 0line 4: 8
}
SourceFile: "Hello.java"

上述文件内容就是利用 javap 反汇编得到的全部信息,这实际上是因为 Class 文件采用了一种类似于 C 中结构体的伪结构来存储数据。


常见字节码指令


⚠大量定义警告⚠:以下将出现大量指令,用到查表即可,建议简单了解加载和存储指令和基本的运算指令即可,重点放在学习最后的方法调用指令实现重写和重载的细节(重中之重)。


加载和存储指令


加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输:

  1. 局部变量加载指令(Load Instructions):

    • iload: 从局部变量表加载一个int类型的变量到操作栈。
    • lload: 从局部变量表加载一个long类型的变量到操作栈。
    • fload: 从局部变量表加载一个float类型的变量到操作栈。
    • dload: 从局部变量表加载一个double类型的变量到操作栈。
    • aload: 从局部变量表加载一个引用类型的变量到操作栈。
  2. 局部变量存储指令(Store Instructions):

    • istore: 将一个int类型的数值从操作数栈存储到局部变量表。
    • lstore: 将一个long类型的数值从操作数栈存储到局部变量表。
    • fstore: 将一个float类型的数值从操作数栈存储到局部变量表。
    • dstore: 将一个double类型的数值从操作数栈存储到局部变量表。
    • astore: 将一个引用类型的数值从操作数栈存储到局部变量表。
  3. 常量加载指令(Constant Instructions):

    • iconst: 将int类型的常量加载到操作数栈。
    • lconst: 将long类型的常量加载到操作数栈。
    • fconst: 将float类型的常量加载到操作数栈。
    • dconst: 将double类型的常量加载到操作数栈。
    • aconst_null: 将null引用加载到操作数栈。
  4. 操作数栈指令(Stack Instructions):

    • bipush: 将一个字节大小的常量加载到操作数栈。
    • sipush: 将一个短整型大小的常量加载到操作数栈。

运算相关指令


  1. 加法指令(Addition Instructions):

    • iadd: 执行两个int类型操作数的相加操作。
    • ladd: 执行两个long类型操作数的相加操作。
    • fadd: 执行两个float类型操作数的相加操作。
    • dadd: 执行两个double类型操作数的相加操作。
  2. 减法指令(Subtraction Instructions):

    • isub: 执行两个int类型操作数的相减操作。
    • lsub: 执行两个long类型操作数的相减操作。
    • fsub: 执行两个float类型操作数的相减操作。
    • dsub: 执行两个double类型操作数的相减操作。
  3. 乘法指令(Multiplication Instructions):

    • imul: 执行两个int类型操作数的相乘操作。
    • lmul: 执行两个long类型操作数的相乘操作。
    • fmul: 执行两个float类型操作数的相乘操作。
    • dmul: 执行两个double类型操作数的相乘操作。
  4. 除法指令(Division Instructions):

    • idiv: 执行两个int类型操作数的相除操作。
    • ldiv: 执行两个long类型操作数的相除操作。
    • fdiv: 执行两个float类型操作数的相除操作。
    • ddiv: 执行两个double类型操作数的相除操作。
  5. 求余指令(Remainder Instructions):

    • irem: 执行两个int类型操作数的求余操作。
    • lrem: 执行两个long类型操作数的求余操作。
    • frem: 执行两个float类型操作数的求余操作。
    • drem: 执行两个double类型操作数的求余操作。
  6. 取反指令(Negation Instructions):

    • ineg: 对一个int类型操作数进行取反操作。
    • lneg: 对一个long类型操作数进行取反操作。
    • fneg: 对一个float类型操作数进行取反操作。
    • dneg: 对一个double类型操作数进行取反操作。
  7. 位移指令(Shift Instructions):

    • ishl: 对一个int类型操作数进行左移操作。
    • ishr: 对一个int类型操作数进行带符号右移操作。
    • iushr: 对一个int类型操作数进行无符号右移操作。
    • lshl: 对一个long类型操作数进行左移操作。
    • lshr: 对一个long类型操作数进行带符号右移操作。
    • lushr: 对一个long类型操作数进行无符号右移操作。
  8. 按位或指令(Bitwise OR Instructions):

    • ior: 对两个int类型操作数进行按位或操作。
    • lor: 对两个long类型操作数进行按位或操作。
  9. 按位与指令(Bitwise AND Instructions):

    • iand: 对两个int类型操作数进行按位与操作。
    • land: 对两个long类型操作数进行按位与操作。
  10. 按位异或指令(Bitwise XOR Instructions):

    • ixor: 对两个int类型操作数进行按位异或操作。
    • lxor: 对两个long类型操作数进行按位异或操作。
  11. 局部变量自增指令(Increment Instructions):

    • iinc: 对一个int类型的局部变量进行自增操作。
  12. 比较指令(Comparison Instructions):

    • dcmpg: 比较两个double类型操作数的大小(带NaN处理)。
    • dcmpl: 比较两个double类型操作数的大小(带NaN处理)。
    • fcmpg: 比较两个float类型操作数的大小(带NaN处理)。
    • fcmpl: 比较两个float类型操作数的大小(带NaN处理)。
    • lcmp: 比较两个long类型操作数的大小。

类型转换指令


  1. 整数类型转换指令(Integer Conversion Instructions):

    • i2l: 将int类型转换为long类型。
    • i2f: 将int类型转换为float类型。
    • i2d: 将int类型转换为double类型。
    • l2i: 将long类型转换为int类型。
    • l2f: 将long类型转换为float类型。
    • l2d: 将long类型转换为double类型。
    • f2i: 将float类型转换为int类型。
    • f2l: 将float类型转换为long类型。
    • f2d: 将float类型转换为double类型。
    • d2i: 将double类型转换为int类型。
    • d2l: 将double类型转换为long类型。
    • d2f: 将double类型转换为float类型。
  2. 类型强制转换指令(Type Casting Instructions):

    • checkcast: 检查对象是否可以强制转换为指定类型。
    • instanceof: 检查对象是否是指定类型的实例。
  3. 数值拓宽和缩窄指令(Numeric Widening and Narrowing Instructions):

    • i2b: 将int类型转换为byte类型。
    • i2c: 将int类型转换为char类型。
    • i2s: 将int类型转换为short类型。

对象创建与访问指令


  1. 对象创建指令(Object Creation Instructions):

    • new: 创建一个新的对象,并将其引用推送到操作数栈上。
    • newarray: 创建一个指定类型的新数组,并将其引用推送到操作数栈上。
    • anewarray: 创建一个引用类型的新数组,并将其引用推送到操作数栈上。
    • multianewarray: 创建一个多维数组,并将其引用推送到操作数栈上。
  2. 对象访问指令(Object Access Instructions):

    • getfield: 从对象中获取实例字段的值,并将其推送到操作数栈上。
    • putfield: 将一个值存储到对象的实例字段中。
    • getstatic: 获取静态字段的值,并将其推送到操作数栈上。
    • putstatic: 将一个值存储到静态字段中。
    • arraylength: 获取数组的长度,并将其推送到操作数栈上。
    • aload: 从局部变量表加载一个引用类型的变量到操作栈上。

操作数栈管理指令


  1. 出栈指令(Pop Instructions):

    • pop: 将操作数栈的栈顶元素出栈。
    • pop2: 将操作数栈的栈顶的一个或两个元素出栈。
  2. 复制指令(Duplicate Instructions):

    • dup: 复制操作数栈的栈顶元素,并将复制值重新压入栈顶。
    • dup2: 复制操作数栈的栈顶的一个或两个元素,并将复制值或双份的复制值重新压入栈顶。
    • dup_x1: 复制操作数栈的栈顶元素,并将复制值插入栈顶下面的一个元素之前。
    • dup2_x1: 复制操作数栈的栈顶的一个或两个元素,并将复制值或双份的复制值插入栈顶下面的一个元素之前。
    • dup_x2: 复制操作数栈的栈顶元素,并将复制值插入栈顶下面的两个元素之前。
    • dup2_x2: 复制操作数栈的栈顶的一个或两个元素,并将复制值或双份的复制值插入栈顶下面的两个元素之前。
  3. 交换指令(Swap Instruction):

    • swap: 将操作数栈最顶端的两个数值互换。

控制转移指令


  1. 条件分支指令:

    • ifeq: 如果栈顶值等于0,则跳转到指定位置。
    • iflt: 如果栈顶值小于0,则跳转到指定位置。
    • ifle: 如果栈顶值小于等于0,则跳转到指定位置。
    • ifne: 如果栈顶值不等于0,则跳转到指定位置。
    • ifgt: 如果栈顶值大于0,则跳转到指定位置。
    • ifge: 如果栈顶值大于等于0,则跳转到指定位置。
    • ifnull: 如果栈顶值为null,则跳转到指定位置。
    • ifnonnull: 如果栈顶值不为null,则跳转到指定位置。
    • if_icmpeq: 如果栈顶两个int值相等,则跳转到指定位置。
    • if_icmpne: 如果栈顶两个int值不相等,则跳转到指定位置。
    • if_icmplt: 如果栈顶第二个int值小于栈顶第一个int值,则跳转到指定位置。
    • if_icmpgt: 如果栈顶第二个int值大于栈顶第一个int值,则跳转到指定位置。
    • if_icmple: 如果栈顶第二个int值小于等于栈顶第一个int值,则跳转到指定位置。
    • if_icmpge: 如果栈顶第二个int值大于等于栈顶第一个int值,则跳转到指定位置。
    • if_acmpeq: 如果栈顶两个引用值相等,则跳转到指定位置。
    • if_acmpne: 如果栈顶两个引用值不相等,则跳转到指定位置。
  2. 复合条件分支指令:

    • tableswitch: 根据一个索引值进行跳转,通过索引值在一张表中查找跳转位置。
    • lookupswitch: 根据一个键值进行跳转,通过键值在一张表中查找跳转位置。
  3. 无条件分支指令:

    • goto: 无条件跳转到指定位置。
    • goto_w: 无条件跳转到指定位置(扩展索引)。
    • jsr: 跳转到指定位置,并将返回地址压入栈顶。
    • jsr_w: 跳转到指定位置(扩展索引),并将返回地址压入栈顶。
    • ret: 返回到指定的局部变量索引位置。

方法返回指令


方法返回指令是根据返回值的类型区分:

  • ireturn:该指令用于返回boolean、byte、char、short和int类型的返回值。它将操作数栈顶的值弹出,并将其作为方法的返回值。然后,将返回值传递给调用该方法的指令。
  • lreturn:该指令用于返回long类型的返回值。它将操作数栈顶的两个元素(高位和低位)弹出,并将它们作为方法的返回值。然后,将返回值传递给调用该方法的指令。
  • freturn:该指令用于返回float类型的返回值。它将操作数栈顶的值弹出,并将其作为方法的返回值。然后,将返回值传递给调用该方法的指令。
  • dreturn:该指令用于返回double类型的返回值。它将操作数栈顶的两个元素(高位和低位)弹出,并将它们作为方法的返回值。然后,将返回值传递给调用该方法的指令。
  • areturn:该指令用于返回引用类型(Object类型)的返回值。它将操作数栈顶的引用值弹出,并将其作为方法的返回值。然后,将返回值传递给调用该方法的指令。
  • return:该指令用于结束方法的执行,并返回到调用该方法的指令。该指令通常用于声明为void的方法、实例初始化方法、类和接口的类初始化方法。它没有返回值。

虚拟机字节码执行引擎


执行引擎概述


我们在 Java 内存模型的篇章已经介绍了虚拟机的运行时区域,不熟悉的朋友可以快速回顾一番 深入理解 JVM 之——Java 内存区域与溢出异常。

image-20230831150214981

那么现在我们应该来讲讲执行引擎了——执行引擎是Java虚拟机核心的组成部分之一

执行引擎(Execution Engine):负责执行加载到内存中的字节码指令,将其转换为机器码并执行。执行引擎通常包括解释器(Interpreter)和即时编译器(Just-In-Time Compiler,JIT)两个部分。

  • 解释器:逐行解释执行字节码指令,相对较慢但具有跨平台的优势。
  • 即时编译器:将热点代码编译为本地机器码,以提高执行速度。即时编译器会根据代码的执行情况进行优化,以获得更好的性能。

在《Java虚拟机规范》中制定了Java虚拟机字节码执行引擎的概念模型,在不同的虚拟机实现中,执行引擎在执行字节码的时候,通常会有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)两种选择,也可能两者兼备,还可能会有同时包含几个不同级别的即时编译器一起工作的执行引擎。

所有的 Java 虚拟机的执行引擎输入、输出都是一致的:输入的是字节码二进制流,处理过程是字节码解析执行的等效过程,输出的是执行结果


运行时的栈帧结构


Java 虚拟机的栈帧是支持方法调用和方法执行的数据结构,它是虚拟机栈的元素之一

栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息

  1. 局部变量表(Local Variable Table):栈帧中的局部变量表用于存储方法中的局部变量和参数。局部变量表是一个数字索引的数组,用于存储各种数据类型的值,包括基本类型和对象引用。

  2. 操作数栈(Operand Stack):栈帧中的操作数栈用于执行方法的操作。它是一个后进先出(LIFO)的栈结构,用于存储方法执行过程中的临时数据和计算结果。操作数栈提供了一组指令,用于对栈顶元素进行操作。

  3. 动态连接(Dynamic Linking):栈帧中的动态连接用于支持方法调用和方法执行过程中的动态链接。它包含了方法的运行时常量池引用,用于在运行时解析符号引用。

  4. 方法返回地址(Return Address):栈帧中的方法返回地址用于记录方法调用完成后的返回位置。当方法执行完成后,程序将返回到该地址继续执行。

重点

  • 栈帧是方法调用和方法执行的基本单位,每个方法在虚拟机栈中都对应一个栈帧。
  • 栈帧的大小在编译阶段就确定,并且与程序运行期变量数据无关。
  • 在执行过程中,只有位于栈顶的方法对应的栈帧是生效的,被称为当前栈帧,与之关联的方法称为当前方法。
  • 执行引擎针对当前栈帧进行操作,执行字节码指令。

字节码的解释执行


Java 语言经常被人们定位为“解释执行”的语言,在Java初生的 JDK1.0 时代,这种定义还算是比较准确的,但当主流的虚拟机中都包含了即时编译器后,Class 文件中的代码到底会被解释执行还是编译执行,就成了只有虚拟机自己才能准确判断的事。


基于栈的指令集和基于寄存器的指令集


我们在前文提到过用 javac 编译器将 .java 文件编译成 .class 文件,这个过程其实就是 Javac 编译器输出的字节码指令流的过程,这个过程实际上是一种基于栈的指令集架构(Instruction Set Architecture,ISA)完成的,通过这种方式得到的字节码指令流里面的指令大部分都是零地址指令,它们依赖操作数栈进行工作

与“基于栈的指令集架构”相对的是“基于寄存器的指令集”,如我们最熟悉的 x86 二地址指令集,这也是当今主流 PC 机中物理硬件直接支持的指令集架构,这些指令依赖寄存器进行工作

例如计算: 1 + 1 1 + 1 1+1 两种指令集看起来是下面这样:

基于栈的指令集:

iconst_1
iconst_1
iadd
istore_0

对于上面的指令:

  1. iconst_1: 将常量值 1 1 1 压入操作数栈顶。
  2. iconst_1: 将常量值 2 2 2 压入操作数栈顶。
  3. iadd: 从操作数栈中弹出两个整数值,将它们相加,并将结果压入操作数栈顶。
  4. istore_0: 将栈顶的整数值存储到局部变量表中索引为 0 0 0 的位置。

基于寄存器的指令集:

mov eax, 1
add eax, 1

对于上面的指令:

  1. mov eax, 1: 将整数值 1 1 1 存储到 eax 寄存器中。
  2. add eax, 1: 这是一个将寄存器 eax 中的值与常量值 1 1 1 相加,并将结果存储回 eax 寄存器。

这两套指令集同时并存和发展,各有优势。

基于栈的指令集:

  1. 优点:
    • 可移植性:由于不直接依赖硬件寄存器,栈指令集更具可移植性,可以在不同的硬件平台上运行。
    • 代码紧凑:栈指令集的指令相对较少,使得生成的字节码更紧凑。
    • 简化编译器实现:栈指令集不需要考虑寄存器分配等问题,编译器的实现相对简单。
  2. 缺点:
    • 执行速度较慢:栈指令集需要频繁进行栈操作和内存访问,内存访问开销大,相对于基于寄存器的指令集执行速度较慢。

基于寄存器的指令集:

  1. 优点:
    • 较高的执行速度:基于寄存器的指令集在执行速度上通常较快,因为寄存器操作速度快,无需频繁的内存访问。
    • 更接近物理机的指令集:主流物理机的指令集都是基于寄存器的,因此基于寄存器的指令集更贴近硬件实现。
  2. 缺点:
    • 硬件依赖性:基于寄存器的指令集直接依赖硬件寄存器,因此在不同的硬件平台上可能存在差异,可移植性较差。
    • 编译器复杂性:基于寄存器的指令集需要考虑寄存器分配等复杂问题,编译器的实现相对较复杂。

基于栈的解释器执行过程


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

public class Test {public static void main(String[] args) {int a = 114;int b = 514;int c = a + b;}
}

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

javac Test.java
javap -v Test.class

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

Classfile /L:/JAVA/BasicSyntax/Learn_JVM/code/Test.classLast modified 202396; size 276 bytesSHA-256 checksum 4064a19d96fe4d72c9d780ef819e1e937b120c31b37482e0b74c70e37c2a5601Compiled from "Test.java"
public class Testminor version: 0major version: 61flags: (0x0021) ACC_PUBLIC, ACC_SUPERthis_class: #7                          // Testsuper_class: #2                         // java/lang/Objectinterfaces: 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: ()Vflags: (0x0001) ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 1: 0public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: (0x0009) ACC_PUBLIC, ACC_STATICCode:stack=2, locals=4, args_size=10: bipush        1142: istore_13: sipush        5146: istore_27: iload_18: iload_29: iadd10: istore_311: returnLineNumberTable:line 3: 0line 4: 3line 5: 7line 6: 11
}
SourceFile: "Test.java"

我们专注于下列信息:

public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: (0x0009) ACC_PUBLIC, ACC_STATICCode:stack=2, locals=4, args_size=10: bipush        1142: istore_13: sipush        5146: istore_27: iload_18: iload_29: iadd10: istore_311: 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 展示解释器的执行过程,其中:

stack=2, locals=4, args_size=1

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

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

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

执行:0: bipush        114
操作数栈状态:[114(栈顶), null]
局部变量表状态:[this(索引起始), null, null, null]
程序计数器状态:0
执行:2: istore_1
操作数栈状态:[null(栈顶), null]
局部变量表状态:[this, 114, null, null]
程序计数器状态:2
执行:3: sipush        514
操作数栈状态:[514(栈顶), null]
局部变量表状态:[this, 114, null, null]
程序计数器状态:3
执行:6: istore_2
操作数栈状态:[nul(栈顶), null]
局部变量表状态:[this, 114, 514, null]
程序计数器状态:6
执行:7: iload_1
操作数栈状态:[114(栈顶), null]
局部变量表状态:[this, 114, 514, null]
程序计数器状态:7
执行:8: iload_2
操作数栈状态:[114(栈顶), 514]
局部变量表状态:[this, 114, 514, null]
程序计数器状态:8
执行:9: iadd
操作数栈状态:[628(栈顶), null]
局部变量表状态:[this, 114, 514, null]
程序计数器状态:9
执行:10: istore_3
操作数栈状态:[null(栈顶), null]
局部变量表状态:[this, 114, 514, 628]
程序计数器状态:10
执行: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”,即应该归入上节的“解析”里去讲解,但部分其他外文资料和国内翻译的许多中文资料都将这种行为称为“静态分派”。

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

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() 方法,运行结果应该是:

hello,gentleman!
hello,lady!

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

hello, guy!
hello, guy!

你先别急,让我先急 🤡

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

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

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

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

Compiled from "StaticDispatch.java"
public class StaticDispatch {public StaticDispatch();Code:0: aload_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: returnpublic 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;)V8: returnpublic 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;)V8: returnpublic 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;)V8: returnpublic static void main(java.lang.String[]);Code:0: new           #25                 // class StaticDispatch$Man3: dup4: invokespecial #27                 // Method StaticDispatch$Man."<init>":()V7: astore_18: new           #28                 // class StaticDispatch$Woman11: dup12: invokespecial #30                 // Method StaticDispatch$Woman."<init>":()V15: astore_216: new           #31                 // class StaticDispatch19: dup20: invokespecial #33                 // Method "<init>":()V23: astore_324: aload_325: aload_126: invokevirtual #34                 // Method sayHello:(LStaticDispatch$Human;)V29: aload_330: aload_231: invokevirtual #34                 // Method sayHello:(LStaticDispatch$Human;)V34: return
}

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

26: invokevirtual #34                 // Method sayHello:(LStaticDispatch$Human;)V
......
31: invokevirtual #34                 // Method sayHello:(LStaticDispatch$Human;)V

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

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

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

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

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);
}

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

hello,gentleman!
hello,lady!

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

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)有着很密切的关联。

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

public class DynamicDispatch {static abstract class Human {protected abstract void sayHello();}static class Man extends Human {@Overrideprotected void sayHello() {System.out.println("man say hello");}}static class Woman extends Human {@Overrideprotected 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() 方法。

理论上运行结果应该为:

man say hello
woman say hello
woman say hello

实际上:

man say hello
woman say hello
woman say hello

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

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

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

Compiled from "DynamicDispatch.java"
public class DynamicDispatch {public DynamicDispatch();Code:0: aload_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: returnpublic static void main(java.lang.String[]);Code:0: new           #7                  // class DynamicDispatch$Man3: dup4: invokespecial #9                  // Method DynamicDispatch$Man."<init>":()V7: astore_18: new           #10                 // class DynamicDispatch$Woman11: dup12: invokespecial #12                 // Method DynamicDispatch$Woman."<init>":()V15: astore_216: aload_117: invokevirtual #13                 // Method DynamicDispatch$Human.sayHello:()V20: aload_221: invokevirtual #13                 // Method DynamicDispatch$Human.sayHello:()V24: new           #10                 // class DynamicDispatch$Woman27: dup28: invokespecial #12                 // Method DynamicDispatch$Woman."<init>":()V31: astore_132: aload_133: invokevirtual #13                 // Method DynamicDispatch$Human.sayHello:()V36: return
}

main 方法中:

Human man = new Man();
Human woman = new Woman();

对应字节码指令为:

 0: new           #7                  // class DynamicDispatch$Man3: dup4: invokespecial #9                  // Method DynamicDispatch$Man."<init>":()V7: astore_18: new           #10                 // class DynamicDispatch$Woman
11: dup
12: invokespecial #12                 // Method DynamicDispatch$Woman."<init>":()V
15: astore_2

而调用方法:

man.sayHello();
woman.sayHello();

对应字节码指令为:

17: invokevirtual #13                 // Method DynamicDispatch$Human.sayHello:()V
......
21: invokevirtual #13                 // Method DynamicDispatch$Human.sayHello:()V

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

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

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

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


单分派与多分派


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

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

对于 Java 来说:

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

invokedynamic


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

我们沿用其中的示例:

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 反编译回去得到下面的内容:

Classfile /L:/JAVA/BasicSyntax/Learn_JVM/code/LambdaTest.classLast modified 202396; size 1445 bytesSHA-256 checksum 3a71d05fe531173bda2fd05e7b9a5a12dc0fd040047f87a59add471744a6a2beCompiled from "LambdaTest.java"
public class LambdaTestminor version: 0major version: 61flags: (0x0021) ACC_PUBLIC, ACC_SUPERthis_class: #38                         // LambdaTestsuper_class: #2                         // java/lang/Objectinterfaces: 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: ()Vflags: (0x0001) ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 1: 0public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: (0x0009) ACC_PUBLIC, ACC_STATICCode:stack=3, locals=2, args_size=10: invokedynamic #7,  0              // InvokeDynamic #0:showTestNumber:()LLambdaTest$Test;5: astore_16: getstatic     #11                 // Field java/lang/System.out:Ljava/io/PrintStream;9: aload_110: ldc           #17                 // int 11451412: 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;)V23: returnLineNumberTable:line 8: 0line 9: 6line 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 LambdaTestpublic static final #74= #70 of #72;    // Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles

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

public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: (0x0009) ACC_PUBLIC, ACC_STATICCode:stack=3, locals=2, args_size=10: invokedynamic #7,  0              // InvokeDynamic #0:showTestNumber:()LLambdaTest$Test;5: astore_16: getstatic     #11                 // Field java/lang/System.out:Ljava/io/PrintStream;9: aload_110: ldc           #17                 // int 11451412: 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;)V23: returnLineNumberTable:line 8: 0line 9: 6line 10: 23
}

其中:

0: invokedynamic #7,  0              // InvokeDynamic #0:showTestNumber:()LLambdaTest$Test;

就对应了我们的:

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.lambda$main$0 方法,这也就是 Lambda 表达式最终在 LambdaTest 中的 main 方法里生成的私有方法。
  6. 最后,由于 invokedynamic 指令在一开始调用了showTestNumber方法,最终将返回的CallSite对象存储在局部变量表中等待调用。

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

相关文章:

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

更好的阅读体验 \huge{\color{red}{更好的阅读体验}} 更好的阅读体验 类文件结构 Write Once&#xff0c;Run Anywhere 对于 C 语言从程序到运行需要经过编译的过程&#xff0c;只有经历了编译后&#xff0c;我们所编写的代码才能够翻译为机器可以直接运行的二进制代码&#x…...

C++:vector

目录 一、关于vector 二、vector的相关函数 三、相关函数的使用 ①构造函数 ②size ③[] ​编辑 ④push_back ⑤迭代器iterator ⑥reserve ⑦resize ⑧find ⑨insert ⑩erase ⑪sort 一、关于vector vector比较像数组 观察可知&#xff0c;vector有两个模板参数…...

Android Automotive编译

系统准备 安装系统 准备一台安装Ubuntu系统的机器&#xff08;windows系统的机器可以通过WSL安装ubuntu系统&#xff09; 安装docker 本文使用docker进行编译&#xff0c;因此提前安装docker。参考网络链接安装docker并设置为不使用sudo进行docker操作。 参考链接&#xff…...

什么是50ETF期权开户条件,怎么开期权交易权限?

50ETF期权是指上证50ETF期权&#xff0c;标的物是上证50ETF&#xff0c;代码是&#xff08;510500&#xff09;&#xff0c;期权是一种在上证50ETF基础上进行衍生品交易的金融工具&#xff0c;下文科普什么是50ETF期权开户条件&#xff0c;怎么开期权交易权限&#xff1f;本文来…...

React 从入门到精通——本文来自AI创作助手

React是一个流行的JavaScript库&#xff0c;用于构建用户界面。以下是React入门到精通的步骤&#xff1a; 入门 安装React 你可以在npm上下载React包&#xff0c;也可以使用其他包管理器。首先需要安装node.js&#xff0c;然后使用以下命令安装React&#xff1a; npm insta…...

【51单片机实验笔记】前篇(三) 模块功能封装汇总(持续更新)

文章目录 通用函数public.hpublic.c 延时函数delay.hdelay.c LED模块数码管模块smg.hsmg.c LED点阵模块独立按键模块矩阵按键模块外部中断模块定时器模块串口通讯模块ADC模块PWM模块 通用函数 包含常用头文件&#xff0c;宏定义&#xff0c;自定义类型&#xff0c;函数工具等。…...

Excel VSTO开发4 -其他事件

版权声明&#xff1a;本文为博主原创文章&#xff0c;转载请在显著位置标明本文出处以及作者网名&#xff0c;未经作者允许不得用于商业目的。 4 其他事件 针对插件的事件主要有Startup、Shutdown这两个事件&#xff0c;在第2节中已经讲解。在开发窗口中&#xff0c;选择对象…...

语音识别数据的采集方法:基本流程数据类型

“人工智能是一种模仿人类功能的产品。数据采集的方法需要针对特定的场景需求。”—–Mark Brayan (澳鹏CEO) 我们一直说&#xff0c;对于一个高质量的人工智能产品离不开高质量的训练数据。对于不同的人工智能我们需要不同的数据对其训练。要采集正确的数据去训练特定的模型才…...

oracle数据库给用户授权DBA权限Oracle查看哪些用户具有DBA权限

oracle数据库给用户授权DBA权限 步骤一&#xff1a;以sysdba身份登录到Oracle数据库 在授予DBA权限之前&#xff0c;我们首先要以sysdba身份登录到Oracle数据库。使用以下命令登录&#xff1a; sqlplus / as sysdba步骤二&#xff1a;创建用户&#xff08;如有用户跳过&#…...

024-从零搭建微服务-系统服务(六)

写在最前 如果这个项目让你有所收获&#xff0c;记得 Star 关注哦&#xff0c;这对我是非常不错的鼓励与支持。 源码地址&#xff08;后端&#xff09;&#xff1a;https://gitee.com/csps/mingyue 源码地址&#xff08;前端&#xff09;&#xff1a;https://gitee.com/csps…...

Arduino驱动TCS3200传感器(颜色传感器篇)

目录 1、传感器特性 2、硬件原理图 3、控制器和传感器连线图 4、驱动程序 TCS3200颜色传感器是一款全彩的颜色检测器,包括了一块TAOS TCS3200RGB感应芯片和4个白色LED灯,TCS3200能在一定的范围内检测和测量几乎所有的可见光。TCS3200有大量的光检测器,每个都有红绿蓝和清…...

基于Matlab实现多个数字水印案例(附上源码+数据集)

数字水印是一种在数字图像或视频中嵌入特定信息的技术&#xff0c;以保护知识产权和防止盗版。在本文中&#xff0c;我们将介绍如何使用Matlab实现数字水印。 文章目录 实现步骤源码数据集下载 实现步骤 首先&#xff0c;我们需要选择一个用于嵌入水印的图像。这可以是原始图像…...

C语言之指针进阶篇(2)

目录 函数指针 函数名和&函数名 函数指针的定义 函数指针的使用 函数指针陷阱 代码1 代码2 注意 函数指针数组定义 函数指针数组的使用 指向函数指针数组的指针 书写 终于军训圆满结束了&#xff0c;首先回顾一下指针进阶篇&#xff08;1&#xff09;主要是…...

C++ 进制转化入门知识(1)

一、什么是进制 进制是一种用来表示数值的系统或方法&#xff0c;它是基于一个特定的基数来工作的。在我们常见的几种进制中&#xff0c;有&#xff1a; 1. **二进制&#xff08;基数 2&#xff09;**&#xff1a; 二进制只用两个数字&#xff1a;0和1。这是计算机内部使用…...

【React】React学习:从初级到高级(四)

React学习[四] 4 应急方案4.1 使用ref引用值4.1.1 给组件添加ref4.1.2 ref和state的不同之处4.1.3 何时使用ref 4.2 使用ref操作DOM4.2.1 获取指向节点的ref4.2.3 使用 ref 回调管理 ref 列表4.2.4 访问另一个组件的DOM节点4.2.5 用 flushSync 同步更新 state 4.3 使用Effect同…...

微信小程序登录问题(思路简略笔记)

配置问题 这是小程序登录问题&#xff0c;必要的两个配置。 流程思路 1. 微信小程序端&#xff0c;会返回一个code。 2. 查看需要返回给微信小程序端的数据。 3. 既然需要返回三个数据&#xff0c;先看openid如何拿到 WX-Login https://api.weixin.qq.com/sns/jscode2ses…...

Go 锁扩展

文章目录 TryLock统计 goroutine数量读写锁读锁写锁常见死锁情况写锁重入写锁中调用读锁循环依赖 TryLock 源码中自带的(我的go是 1.20版本)TryLock 会尝试获取锁&#xff0c;如果获取不到返回false&#xff0c;并不会进行休眠阻塞(和 Lock的主要区别) func (m *Mutex) TryLo…...

Docker的简介及安装

[shouce]http://shouce.jb51.net/docker_practice/栾一峰菜鸟教程参考文献 1 环境配置的难题 软件开发最大的麻烦事之一&#xff0c;就是环境配置。用户计算机的环境都不相同&#xff0c;你怎么知道自家的软件&#xff0c;能在那些机器跑起来&#xff1f; 用户必须保证两件事…...

安卓核心板的不同核心规格及架构介绍

安卓核心板是将核心功能封装的一块电子主板&#xff0c;集成芯片、存储器和功放器件等&#xff0c;并提供标准接口的芯片。 其特点&#xff1a; ● 能跑 Android 等操作系统 强大的功能及丰富的接口 支持 LCD/TP&#xff0c;Audio&#xff0c;Camera&#xff0c;Video&#…...

flume1.11.0安装部署

1、准备安装包apache-flume-1.11.0-bin.tar.gz&#xff1b; 上传&#xff1b; 2、安装flume-1.11.0&#xff1b; 解压&#xff1b; tar -zxvf apache-flume-1.11.0-bin.tar.gz -C /opt/server 进入conf目录&#xff0c;修改flume-env.sh&#xff0c;配置JAVA_HOME&#xff1b…...

通过wordpress 自定义主题的额外CSS删除指定区块

最近用wordpress建站&#xff0c;想要删除指定区块&#xff0c;发现相关的教程蛮少的&#xff0c;作为小白的我搜了相关教程&#xff0c;好像没找到&#xff0c;只能自己慢慢摸索了&#xff0c;看了很多&#xff0c;终于尝试实现了&#xff0c;特记录下&#xff0c;免得自己忘了…...

Rokid Jungle--Max pro

介绍和功能开发 YodaOS-Master操作系统&#xff1a;以交换计算为核心&#xff0c;实现单目SLAM空间交互&#xff0c;具有高精度、实时性和稳定性。发布UXR2.0SDK&#xff0c;为构建空间内容提供丰富的开发套件 多模态交互 算法原子化 多种开发工具协同 多生态支持 骁龙XR2…...

【LeetCode算法系列题解】第61~65题

CONTENTS LeetCode 61. 旋转链表&#xff08;中等&#xff09;LeetCode 62. 不同路径&#xff08;中等&#xff09;LeetCode 63. 不同路径 II&#xff08;中等&#xff09;LeetCode 64. 最小路径和&#xff08;中等&#xff09;LeetCode 65. 有效数字&#xff08;困难&#xff…...

MATLAB中fillmissing函数用法

目录 语法 说明 示例 包含 NaN 值的向量 由 NaN 值组成的矩阵 插入缺失数据 使用移动中位数方法 使用自定义填充方法 包含缺失端点的矩阵 包含多个数据类型的表 fillmissing函数的功能是填充缺失的条目。 语法 F fillmissing(A,constant,v) F fillmissing(A,meth…...

电脑同时连接有线和无线网络怎么设置网络的优先级

电脑同时连接有线和无线网络怎么设置网络的优先级&#xff1a; 我们知道在 笔记本电脑系统 中&#xff0c;可以通过有线或无线网络进行联网。如果电脑在有线网络和无线网络同时存在的情况&#xff0c;应该怎么设置有线网络优先连接呢?对此我们提供下面的方法可以让电脑在有Wi…...

el-form表单动态校验(场景: 输入框根据单选项来动态校验表单 没有选中的选项就不用校验)

el-form表单动态校验 el-form常规校验方式: // 结构部分 <el-form ref"form" :model"form" :rules"rules"><el-form-item label"活动名称: " prop"name" required><el-input v-model"form.name" /…...

Java 数据结构与算法应该如何学习?

学习数据结构是计算机科学和软件工程领域中的重要基础知识之一。掌握数据结构对于编写高效、可扩展和可维护的代码至关重要。 1、掌握基本概念 首先&#xff0c;你需要掌握数据结构的基本概念。了解不同类型的数据结构&#xff0c;如数组、链表、栈、队列、树、图等&#xff…...

力扣(LeetCode)算法_C++——有效的数独

请你判断一个 9 x 9 的数独是否有效。只需要 根据以下规则 &#xff0c;验证已经填入的数字是否有效即可。 数字 1-9 在每一行只能出现一次。 数字 1-9 在每一列只能出现一次。 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。&#xff08;请参考示例图&#xff09; …...

制造企业如何优化物料控制?

导 读 ( 文/ 2127 ) 物料控制是指对制造过程中所涉及的物料流动和库存进行有效管理和控制的过程。它包括物料需求计划、供应商管理、物料采购、物料接收和入库、物料库存管理以及物料发放和使用等关键环节。通过精确的物料需求计划和库存管理&#xff0c;物料控制可以确保物料供…...

《Go语言在微服务中的崛起:为什么Go是下一个后端之星?》

&#x1f337;&#x1f341; 博主猫头虎&#x1f405;&#x1f43e; 带您进入 Golang 语言的新世界✨✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文并茂&#x1f…...

个人小说网站怎么做/站长工具果冻传媒

利用Value注解将对应的属性注入到field上 Value("${com.Code}")private String comCode;...

怎么自己编码做网站/上海自媒体推广

转载于:https://www.cnblogs.com/tszr/p/10127036.html...

免费项目网站/最有效的广告宣传方式

transforms包含了一些常用的图像变换&#xff0c;这些变换能够用Compose串联组合起来。 # 用于把一系列变换组合到一起。 class torchvision.transforms.Compose(transforms)# 格式变换&#xff0c;把张量或ndarray转化为PIL图像。 class torchvision.transforms.ToPILImage(m…...

做外贸常用网站/网站更换服务器对seo的影响

Adaptive pooling 是一种对图像进行重采样的方法&#xff0c;它可以根据输入图像的大小自适应地调整输出图像的大小。这种方法通常用于深度学习中&#xff0c;用来将不同大小的输入图像转换为统一的大小&#xff0c;以便进行处理。...

wordpress 本地编辑器/信息发布平台推广有哪些

需求&#xff1a;1&#xff0c;外层根据参与人数排序&#xff0c;并据此分页 2&#xff0c;内层首先根据级别排序&#xff0c;级别相同时&#xff0c;按照票数排序&#xff0c;固定展示8条 实现基础&#xff1a;ArrayList 可能的坑&#xff1a;仅仅对最外层list执行一次排序调…...

wordpress的页面标题/seo的主要工作内容

/*示例 创建触发器*/--需求&#xff1a;当职工表emp 被删除一条记录时&#xff0c;把被删除记录写到删除日志表中去CREATE TABLE del_emp AS SELECT * FROM EMP WHERE 12; --创建触发器CREATE OR REPLACE TRIGGER tr_del_emp BEFORE DELETE --指定触发时机为删除操作前触发ON e…...