深入理解 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 方法的动态调用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/126308.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

230. 二叉搜索树中第K小的元素

230. 二叉搜索树中第K小的元素 C代码&#xff1a;二叉树 int kthSmallest(struct TreeNode* root, int k){// struct TreeNode** stack malloc(sizeof(struct TreeNode*) * 10000); // root 是结构体的地址struct TreeNode* stack[10000];int stkTop 0;while (root ! NULL …

mysql 查询优化 、索引失效

查询优化 物理查询优化 通过索引和表连接方式等技术来进行优化&#xff0c;这里重点需要掌握索引的使用 逻辑查询优化 通过SQL 等价变换 提升查询效率&#xff0c;直白一点就是说&#xff0c;换一种查询写法执行效率可能更高 索引失效 计算、函数、类型转换&#xff08;自动或…

FasterNet(PConv)paper笔记(CVPR2023)

论文&#xff1a;Run, Don’t Walk: Chasing Higher FLOPS for Faster Neural Networks 先熟悉两个概念&#xff1a;FLOPS和FLOPs&#xff08;s一个大写一个小写&#xff09; FLOPS: FLoating point Operations Per Second的缩写&#xff0c;即每秒浮点运算次数&#xff0c;或…

【计算机网络】 确认应答机制与超时重传

文章目录 ACK机制——确认应答机制超时重传 ACK机制——确认应答机制 当我们客户端发送了一个数据&#xff0c;seq是1100&#xff0c;那么服务端在收到时就会回一个ack101的ACK包&#xff0c;代表101之前的包我都收到了&#xff0c;下面请你从101继续发送。然后客户端就会发送1…

Alins - 化繁为简、极致优雅的WebUI框架

最近造了个js框架 Alins&#xff0c;分享一下&#xff1a; &#x1f680; Alins: 最纯粹优雅的WebUI框架 English | 文档 | 演练场 | 更新日志 | 反馈错误/缺漏 | Gitee | 留言板 0 简介 0.1 前言 Alins是一款极致纯粹、简洁、优雅的Web UI框架。秉持0-API、Less is More 的…

慕尼黑主题活动!亚马逊云科技生成式AI全新解决方案,引领未来移动出行领域

IAA作为世界五大车展之一&#xff0c;一直对全球汽车产业的发展起着关键作用&#xff01;2023年9月5日在慕尼黑开幕的IAA MOBILITY 2023以“体验联动智慧出行”为主题&#xff0c;紧跟移动出行领域的前沿变化&#xff0c;将汇集整车企业、开发者、供应商、科技公司、服务提供商…

【小沐学Unity3d】3ds Max 多维子材质编辑(Multi/Sub-object)

文章目录 1、简介2、精简材质编辑器2.1 先创建多维子材质&#xff0c;后指定它2.2 先指定标准材质&#xff0c;后自动创建多维子材质 3、Slate材质编辑器3.1 编辑器简介3.2 编辑器使用 结语 1、简介 多维子材质&#xff08;Multi/Sub-object&#xff09;是为一个模形&#xff0…

tab切换,左右加箭头,点击箭头实现tab切换

和正常tab切换一样原理&#xff0c;点击箭头多了步计算 <template><div><div class"tab-container"><p>{{projectName}}</p><div class"banner"><div v-for"(tab, index) in tabs" :key"index&quo…

物联网世界的无线电报之MQTT详解

文章目录 1. 前言1.1. 物联网与MQTT的关系1.2. MQTT的重要性及应用场景 2. MQTT基础2.1. MQTT的定义与起源2.2. MQTT的工作原理2.3. MQTT的协议格式2.4. 用java造个轮子 3. 深入理解MQTT3.1. MQTT的主要组件3.1.1. Publisher&#xff08;发布者&#xff09;3.1.2. Subscriber&a…

群晖NAS:通过Docker 部署宝塔面板【注册表:cyberbolt/baota】

群晖NAS&#xff1a;通过 Docker 部署宝塔面板【注册表&#xff1a;pch18/baota】 由于 docker 源地址被墙&#xff0c;在面板里面查询不到注册表&#xff0c;使用 ssh 命令行拉取 1、打开 SSH&#xff0c;链接后打开命令行 这里不赘述&#xff0c;具体自行百度 2、下载 镜像…

笔试记录-扔鸡蛋问题

写目录 一个鸡蛋两个鸡蛋K个鸡蛋 今天面试官问了我这个扔鸡蛋问题&#xff0c;以前学过&#xff0c;但是面试的时候想不起来了&#xff0c;应该是直接寄了&#xff0c;接下来总结一下这个问题的动态规划做法. 问题&#xff1a;有一个N层高的楼&#xff0c;现在给你若干个鸡蛋&a…

华为云云耀云服务器L实例评测|使用宝塔面板管理服务器,并搭建个人博客网站

作者简介&#xff1a;一名云计算网络运维人员、每天分享网络与运维的技术与干货。 公众号&#xff1a;网络豆 座右铭&#xff1a;低头赶路&#xff0c;敬事如仪 个人主页&#xff1a; 网络豆的主页​​​​​ 目录 前言 介绍&#xff1a; 一.购买使用华为云云耀服务器 …

【postgresql 基础入门】数据库服务的管理,启动、停止、状态查看、配置加载、重启都在这里

数据库服务管理 ​专栏内容&#xff1a; postgresql内核源码分析手写数据库toadb并发编程 ​开源贡献&#xff1a; toadb开源库 个人主页&#xff1a;我的主页 管理社区&#xff1a;开源数据库 座右铭&#xff1a;天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff…

解决css设置图片大小不生效的问题

今天在做css布局时发现一个问题&#xff1a;设置图片大小不生效&#xff1a; 如上图所示&#xff1a;左上角两个图标的大小不一致&#xff0c;第一个是56x56,第二个是49x49,所以要把第二个的高度设置成56px&#xff1a; .mi-home img {height: 56px; }但是如上代码&#xff0c;…

R语言STAN贝叶斯线性回归模型分析气候变化影响北半球海冰范围和可视化检查模型收敛性...

原文链接&#xff1a;http://tecdat.cn/?p24334 像任何统计建模一样&#xff0c;贝叶斯建模可能需要为你的研究问题设计合适的模型&#xff0c;然后开发该模型&#xff0c;使其符合你的数据假设并运行&#xff08;点击文末“阅读原文”获取完整代码数据&#xff09;。 相关视频…

Python 03(循环语句)

Python03&#xff08;循环语句&#xff09; 文章目录 Python03&#xff08;循环语句&#xff09;一、while语句二、while实现猜数字三、while循环的嵌套while循环嵌套实例需求&#xff1a; 四、for循环1、什么 是for循环2、语法3、执行流程4、for循环的基本使用5、range()函数6…

【C++从0到王者】第二十六站:一些经典的多态面试题

文章目录 前言一、多态的常见选择二、多态的常见问答总结 前言 多态是C的一大疑难杂症&#xff0c;有很多细枝末节的东西非常繁琐&#xff0c;这里搜集了一些常见的选择与问答。可以为大家带来帮助理解多态 一、多态的常见选择 下面哪种面向对象的方法可以让你变得富有( ) A: …

WireShark抓包工具的安装

1.下载安装包 在官网或者电脑应用商城都可以下载 2.安装 打开安装包&#xff0c;点击next 点击next 选择UI界面&#xff0c;两种都装上 根据习惯选择 选择安装位置点击安装 开始安装安装成功

生成订单30分钟未支付,则自动取消,该怎么实现?

今天给大家上一盘硬菜&#xff0c;并且是支付中非常重要的一个技术解决方案&#xff0c;有这块业务的同学注意自己试一把了哈&#xff01; 在开发中&#xff0c;往往会遇到一些关于延时任务的需求。例如 生成订单30分钟未支付&#xff0c;则自动取消 生成订单60秒后,给用户…

ArcGIS API for JavaScript 4.x 实现动态脉冲效果

1. 设计思路 主要通过定时刷新&#xff0c;每一次的脉冲渲染圈不停的放大&#xff0c;并且透明度缩小&#xff0c;直到达到一定的大小再退回0。 2. 实现代码 import MapView from "arcgis/core/views/MapView"; import GraphicsLayer from "arcgis/core/laye…