网站空间 按流量计费,马云不会代码怎么做的网站,东阳网站优化,wordpress yii更好的阅读体验 \huge{\color{red}{更好的阅读体验}} 更好的阅读体验
类文件结构 Write Once#xff0c;Run Anywhere 对于 C 语言从程序到运行需要经过编译的过程#xff0c;只有经历了编译后#xff0c;我们所编写的代码才能够翻译为机器可以直接运行的二进制代码#x… 更好的阅读体验 \huge{\color{red}{更好的阅读体验}} 更好的阅读体验
类文件结构 Write OnceRun 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可以看到不出意外地输出了 这样我们就通过解释的方式运行了刚刚编写的 .java 文件但为了更好的理解 Java 的类文件结构我们接下来对 .java 文件进行编译
javac Hello.java可以看到在当前目录下生成了一个 Hello.class 文件 接下来我们运行
java Hello可以看到不出意外的输出了 这样我们就以运行编译后的文件方式运行了 .class 文件。
如果我们以十六进制打开该文件 你会看到上述文件充满了以两个字节为一组的字节码前两组四个字节被成为“魔数”说明该文件是否为一个能被虚拟机接受的 Class 文件而 Java 的 Class 文件的魔数值为 0xCAFEBABE ☕。
“一次编写到处运行”——这是 Java 在刚刚诞生之时提出的非常著名的宣传口号而 Java 的解决方案便是将像上面这样的源代码编译为平台无关的中间格式——字节码文件.class 文件并通过对应的 Java 虚拟机读取和运行这些中间格式的编译文件这就使得语言本身实现了跨平台。 类文件结构概述 Java 源代码经过编译器编译后会生成类似上面的字节码文件.class 文件其中字节码文件的结构如下 魔数Magic Number字节码文件的前四个字节是一个固定的魔数0xCAFEBABE用于标识该文件为 Java 字节码文件。 版本信息Version紧随魔数之后的两个字节表示字节码文件的版本信息分别是主版本号和次版本号。 常量池Constant Pool紧随版本信息之后是一个常量池表Constant Pool Table用于存储编译时生成的各种常量、符号引用和字面量。常量池中的每个项目都有一个索引通过索引可以在运行时查找和使用。 访问标志Access Flags紧随常量池之后的两个字节表示类或接口的访问标志用于指示该类或接口的访问级别和特性。 类索引、父类索引和接口索引Class Indexes紧随访问标志之后的两个字节表示类索引用于指向常量池中该类的全限定名。接着是两个字节的父类索引用于指向常量池中父类的全限定名。接口索引表紧随父类索引之后用于指向常量池中实现的接口的全限定名。 字段表Field Table紧随接口索引之后是字段表用于描述类或接口中定义的字段的访问标志、字段名、字段类型等信息。 方法表Method Table紧随字段表之后是方法表用于描述类或接口中定义的方法的访问标志、方法名、方法参数、方法返回类型等信息。 属性表Attribute Table紧随方法表之后是属性表用于存储与类、字段或方法相关的附加信息如注解、代码行号表、异常表等。 字节码指令 生成反编译文件 由于字节码实在不是人读的难以理解如果你感兴趣全部读懂可以自行深入学习 真的有人用纯字节码编程吗我们还是康康能理解一点的东西罢
我们利用 javap 命令对 .class 文件反编译将其输出到文本
javap -v Hello.class Hello.txtjavap 命令的一些常用选项包括 -c显示字节码指令。-l显示行号和本地变量表。-s显示内部类型签名。-verbose显示所有信息可简化为 -v。 然后打开 Hello.txt 查看
Classfile /L:/JAVA/BasicSyntax/Learn_JVM/code/Hello.classLast modified 2023年9月5日; 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:stack1, locals1, args_size10: 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:stack2, locals1, args_size10: 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 中结构体的伪结构来存储数据。 常见字节码指令 ⚠大量定义警告⚠以下将出现大量指令用到查表即可建议简单了解加载和存储指令和基本的运算指令即可重点放在学习最后的方法调用指令实现重写和重载的细节重中之重。 加载和存储指令 加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输 局部变量加载指令Load Instructions iload: 从局部变量表加载一个int类型的变量到操作栈。lload: 从局部变量表加载一个long类型的变量到操作栈。fload: 从局部变量表加载一个float类型的变量到操作栈。dload: 从局部变量表加载一个double类型的变量到操作栈。aload: 从局部变量表加载一个引用类型的变量到操作栈。 局部变量存储指令Store Instructions istore: 将一个int类型的数值从操作数栈存储到局部变量表。lstore: 将一个long类型的数值从操作数栈存储到局部变量表。fstore: 将一个float类型的数值从操作数栈存储到局部变量表。dstore: 将一个double类型的数值从操作数栈存储到局部变量表。astore: 将一个引用类型的数值从操作数栈存储到局部变量表。 常量加载指令Constant Instructions iconst: 将int类型的常量加载到操作数栈。lconst: 将long类型的常量加载到操作数栈。fconst: 将float类型的常量加载到操作数栈。dconst: 将double类型的常量加载到操作数栈。aconst_null: 将null引用加载到操作数栈。 操作数栈指令Stack Instructions bipush: 将一个字节大小的常量加载到操作数栈。sipush: 将一个短整型大小的常量加载到操作数栈。 运算相关指令 加法指令Addition Instructions iadd: 执行两个int类型操作数的相加操作。ladd: 执行两个long类型操作数的相加操作。fadd: 执行两个float类型操作数的相加操作。dadd: 执行两个double类型操作数的相加操作。 减法指令Subtraction Instructions isub: 执行两个int类型操作数的相减操作。lsub: 执行两个long类型操作数的相减操作。fsub: 执行两个float类型操作数的相减操作。dsub: 执行两个double类型操作数的相减操作。 乘法指令Multiplication Instructions imul: 执行两个int类型操作数的相乘操作。lmul: 执行两个long类型操作数的相乘操作。fmul: 执行两个float类型操作数的相乘操作。dmul: 执行两个double类型操作数的相乘操作。 除法指令Division Instructions idiv: 执行两个int类型操作数的相除操作。ldiv: 执行两个long类型操作数的相除操作。fdiv: 执行两个float类型操作数的相除操作。ddiv: 执行两个double类型操作数的相除操作。 求余指令Remainder Instructions irem: 执行两个int类型操作数的求余操作。lrem: 执行两个long类型操作数的求余操作。frem: 执行两个float类型操作数的求余操作。drem: 执行两个double类型操作数的求余操作。 取反指令Negation Instructions ineg: 对一个int类型操作数进行取反操作。lneg: 对一个long类型操作数进行取反操作。fneg: 对一个float类型操作数进行取反操作。dneg: 对一个double类型操作数进行取反操作。 位移指令Shift Instructions ishl: 对一个int类型操作数进行左移操作。ishr: 对一个int类型操作数进行带符号右移操作。iushr: 对一个int类型操作数进行无符号右移操作。lshl: 对一个long类型操作数进行左移操作。lshr: 对一个long类型操作数进行带符号右移操作。lushr: 对一个long类型操作数进行无符号右移操作。 按位或指令Bitwise OR Instructions ior: 对两个int类型操作数进行按位或操作。lor: 对两个long类型操作数进行按位或操作。 按位与指令Bitwise AND Instructions iand: 对两个int类型操作数进行按位与操作。land: 对两个long类型操作数进行按位与操作。 按位异或指令Bitwise XOR Instructions ixor: 对两个int类型操作数进行按位异或操作。lxor: 对两个long类型操作数进行按位异或操作。 局部变量自增指令Increment Instructions iinc: 对一个int类型的局部变量进行自增操作。 比较指令Comparison Instructions dcmpg: 比较两个double类型操作数的大小带NaN处理。dcmpl: 比较两个double类型操作数的大小带NaN处理。fcmpg: 比较两个float类型操作数的大小带NaN处理。fcmpl: 比较两个float类型操作数的大小带NaN处理。lcmp: 比较两个long类型操作数的大小。 类型转换指令 整数类型转换指令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类型。 类型强制转换指令Type Casting Instructions checkcast: 检查对象是否可以强制转换为指定类型。instanceof: 检查对象是否是指定类型的实例。 数值拓宽和缩窄指令Numeric Widening and Narrowing Instructions i2b: 将int类型转换为byte类型。i2c: 将int类型转换为char类型。i2s: 将int类型转换为short类型。 对象创建与访问指令 对象创建指令Object Creation Instructions new: 创建一个新的对象并将其引用推送到操作数栈上。newarray: 创建一个指定类型的新数组并将其引用推送到操作数栈上。anewarray: 创建一个引用类型的新数组并将其引用推送到操作数栈上。multianewarray: 创建一个多维数组并将其引用推送到操作数栈上。 对象访问指令Object Access Instructions getfield: 从对象中获取实例字段的值并将其推送到操作数栈上。putfield: 将一个值存储到对象的实例字段中。getstatic: 获取静态字段的值并将其推送到操作数栈上。putstatic: 将一个值存储到静态字段中。arraylength: 获取数组的长度并将其推送到操作数栈上。aload: 从局部变量表加载一个引用类型的变量到操作栈上。 操作数栈管理指令 出栈指令Pop Instructions pop: 将操作数栈的栈顶元素出栈。pop2: 将操作数栈的栈顶的一个或两个元素出栈。 复制指令Duplicate Instructions dup: 复制操作数栈的栈顶元素并将复制值重新压入栈顶。dup2: 复制操作数栈的栈顶的一个或两个元素并将复制值或双份的复制值重新压入栈顶。dup_x1: 复制操作数栈的栈顶元素并将复制值插入栈顶下面的一个元素之前。dup2_x1: 复制操作数栈的栈顶的一个或两个元素并将复制值或双份的复制值插入栈顶下面的一个元素之前。dup_x2: 复制操作数栈的栈顶元素并将复制值插入栈顶下面的两个元素之前。dup2_x2: 复制操作数栈的栈顶的一个或两个元素并将复制值或双份的复制值插入栈顶下面的两个元素之前。 交换指令Swap Instruction swap: 将操作数栈最顶端的两个数值互换。 控制转移指令 条件分支指令 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: 如果栈顶两个引用值不相等则跳转到指定位置。 复合条件分支指令 tableswitch: 根据一个索引值进行跳转通过索引值在一张表中查找跳转位置。lookupswitch: 根据一个键值进行跳转通过键值在一张表中查找跳转位置。 无条件分支指令 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 内存区域与溢出异常。 那么现在我们应该来讲讲执行引擎了——执行引擎是Java虚拟机核心的组成部分之一。
执行引擎Execution Engine负责执行加载到内存中的字节码指令将其转换为机器码并执行。执行引擎通常包括解释器Interpreter和即时编译器Just-In-Time CompilerJIT两个部分。
解释器逐行解释执行字节码指令相对较慢但具有跨平台的优势。即时编译器将热点代码编译为本地机器码以提高执行速度。即时编译器会根据代码的执行情况进行优化以获得更好的性能。 在《Java虚拟机规范》中制定了Java虚拟机字节码执行引擎的概念模型在不同的虚拟机实现中执行引擎在执行字节码的时候通常会有解释执行通过解释器执行和编译执行通过即时编译器产生本地代码执行两种选择也可能两者兼备还可能会有同时包含几个不同级别的即时编译器一起工作的执行引擎。 所有的 Java 虚拟机的执行引擎输入、输出都是一致的输入的是字节码二进制流处理过程是字节码解析执行的等效过程输出的是执行结果。 运行时的栈帧结构 Java 虚拟机的栈帧是支持方法调用和方法执行的数据结构它是虚拟机栈的元素之一。
栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息 局部变量表Local Variable Table栈帧中的局部变量表用于存储方法中的局部变量和参数。局部变量表是一个数字索引的数组用于存储各种数据类型的值包括基本类型和对象引用。 操作数栈Operand Stack栈帧中的操作数栈用于执行方法的操作。它是一个后进先出LIFO的栈结构用于存储方法执行过程中的临时数据和计算结果。操作数栈提供了一组指令用于对栈顶元素进行操作。 动态连接Dynamic Linking栈帧中的动态连接用于支持方法调用和方法执行过程中的动态链接。它包含了方法的运行时常量池引用用于在运行时解析符号引用。 方法返回地址Return Address栈帧中的方法返回地址用于记录方法调用完成后的返回位置。当方法执行完成后程序将返回到该地址继续执行。
重点
栈帧是方法调用和方法执行的基本单位每个方法在虚拟机栈中都对应一个栈帧。栈帧的大小在编译阶段就确定并且与程序运行期变量数据无关。在执行过程中只有位于栈顶的方法对应的栈帧是生效的被称为当前栈帧与之关联的方法称为当前方法。执行引擎针对当前栈帧进行操作执行字节码指令。 字节码的解释执行 Java 语言经常被人们定位为“解释执行”的语言在Java初生的 JDK1.0 时代这种定义还算是比较准确的但当主流的虚拟机中都包含了即时编译器后Class 文件中的代码到底会被解释执行还是编译执行就成了只有虚拟机自己才能准确判断的事。 基于栈的指令集和基于寄存器的指令集 我们在前文提到过用 javac 编译器将 .java 文件编译成 .class 文件这个过程其实就是 Javac 编译器输出的字节码指令流的过程这个过程实际上是一种基于栈的指令集架构Instruction Set ArchitectureISA完成的通过这种方式得到的字节码指令流里面的指令大部分都是零地址指令它们依赖操作数栈进行工作。
与“基于栈的指令集架构”相对的是“基于寄存器的指令集”如我们最熟悉的 x86 二地址指令集这也是当今主流 PC 机中物理硬件直接支持的指令集架构这些指令依赖寄存器进行工作。
例如计算 1 1 1 1 11 两种指令集看起来是下面这样
基于栈的指令集
iconst_1
iconst_1
iadd
istore_0对于上面的指令
iconst_1: 将常量值 1 1 1 压入操作数栈顶。iconst_1: 将常量值 2 2 2 压入操作数栈顶。iadd: 从操作数栈中弹出两个整数值将它们相加并将结果压入操作数栈顶。istore_0: 将栈顶的整数值存储到局部变量表中索引为 0 0 0 的位置。
基于寄存器的指令集
mov eax, 1
add eax, 1对于上面的指令
mov eax, 1: 将整数值 1 1 1 存储到 eax 寄存器中。add eax, 1: 这是一个将寄存器 eax 中的值与常量值 1 1 1 相加并将结果存储回 eax 寄存器。
这两套指令集同时并存和发展各有优势。
基于栈的指令集
优点 可移植性由于不直接依赖硬件寄存器栈指令集更具可移植性可以在不同的硬件平台上运行。代码紧凑栈指令集的指令相对较少使得生成的字节码更紧凑。简化编译器实现栈指令集不需要考虑寄存器分配等问题编译器的实现相对简单。 缺点 执行速度较慢栈指令集需要频繁进行栈操作和内存访问内存访问开销大相对于基于寄存器的指令集执行速度较慢。
基于寄存器的指令集
优点 较高的执行速度基于寄存器的指令集在执行速度上通常较快因为寄存器操作速度快无需频繁的内存访问。更接近物理机的指令集主流物理机的指令集都是基于寄存器的因此基于寄存器的指令集更贴近硬件实现。 缺点 硬件依赖性基于寄存器的指令集直接依赖硬件寄存器因此在不同的硬件平台上可能存在差异可移植性较差。编译器复杂性基于寄存器的指令集需要考虑寄存器分配等复杂问题编译器的实现相对较复杂。 基于栈的解释器执行过程 接下看我们具体看一个实际的代码示例
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 2023年9月6日; 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:stack1, locals1, args_size10: 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:stack2, locals4, args_size10: 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:stack2, locals4, args_size10: 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 展示解释器的执行过程其中
stack2, locals4, args_size1提示我们这段代码需要深度为 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上面的执行过程仅仅是一种概念模型虚拟机最终会对执行过程做出一系列优化来提高性能实际的运作过程并不会完全符合概念模型的描述。
更确切地说实际情况会和上面描述的概念模型差距非常大差距产生的根本原因是虚拟机中解析器和即时编译器都会对输入的字节码进行优化即使解释器中也不是按照字节码指令去逐条执行的。
关于编译器优化的细节我们会在以后的系列文章中提到。 方法调用指令 指令概述 针对调用不同类型的方法字节码指令集里设计了不同的指令 invokestatic用于调用静态方法。可以在类加载时将符号引用解析为直接引用。 invokespecial用于调用实例构造器 init() 方法、私有方法和父类中的方法。也可以在类加载时将符号引用解析为直接引用。 invokevirtual用于调用所有的虚方法。根据对象的实际类型进行分派虚方法分派。 invokeinterface用于调用接口方法会在运行时确定实现该接口的对象并选择适合的方法进行调用。 invokedynamic先在运行时动态解析出调用点限定符所引用的方法然后再执行该方法。分派逻辑由用户设定的引导方法决定。
这些调用指令可以根据对象的类型和方法的特性进行不同的分派和调用。 invokedynamic 指令是在 JDK 7时加入到字节码中的当时确实只为了做动态语言如 JRuby、Scala支持Java 语言本身并不会用到它。而到了JDK 8 时代Java 有了 Lambda 表达式和接口的默认方法它们在底层调用时就会用到 invokedynamic 指令。 其中invokestatic 和 invokespecial 指令可以调用非虚方法包括静态方法、私有方法、实例构造器和父类方法。而 invokevirtual 和 invokeinterface 指令用于调用虚方法根据对象的实际类型进行分派。
也许这些指令看起来简单但很难理解这是因为我们在上文多次提到过“方法调用”、“解析”、“分派”这些东西别急如果想要真正弄清楚这些指令我们需要一步步来 方法调用概述 方法调用并不等同于方法中的代码被执行方法调用阶段唯一的任务就是确定被调用方法的版本即调用哪一个方法暂时还未涉及方法内部的具体运行过程。
一切方法调用在 Class 文件里面存储的都只是符号引用而不是方法在实际运行时内存布局中的入口地址直接引用。
这个特性给 Java 带来了更强大的动态扩展能力但也使得 Java 方法调用过程变得相对复杂某些调用需要在类加载期间甚至到运行期间才能确定目标方法的直接引用。
在 Javav 中方法调用过程中同时存在解析Resolution和分派Dispatch两个过程方法调用过程中首先进行解析将符号引用转化为直接引用然后根据实际对象的类型进行分派确定方法的实际执行版本。 解析 解析
在类加载的解析阶段将方法调用的符号引用转化为直接引用的过程。
解析的前提
方法在程序编写、编译阶段就有一个可确定的调用版本并且这个版本在运行期是不可改变的。必须是静态方法、私有方法和被 final 修饰的实例方法因为它们都不可能通过继承或其他方式重写出其他版本。
解析调用过程
解析调用是静态的过程在编译期间就完全确定不延迟到运行期再完成。在类加载的解析阶段涉及的符号引用会被转变为明确的直接引用存储在常量池中。
这种转化使得方法调用在运行时可以更高效地执行无需再进行符号解析直接使用已经解析的直接引用。 分派重点 Java 作为一门面向对象的编程语言具备继承、封装和多态这三个基本特征。
而分派调用过程在 Java 虚拟机中揭示了多态性的体现特别是在方法的重载和重写方面 重载Overloading重载是指在同一个类中定义多个方法它们具有相同的名称但参数列表不同。在虚拟机中实现重载时会根据方法调用的静态类型声明类型选择合适的方法版本。这属于静态分派根据参数的静态类型来确定方法的版本。 重写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 的子类 Man 和 Woman。类中定义了三个重载的 sayHello 方法分别接受 Human、Man 和 Woman 类型的参数并输出相应的问候语。
理论上我们重载了 sayHello() 方法运行结果应该是
hello,gentleman!
hello,lady!但实际上控制台哼哼哼啊啊啊地输出了
hello, guy!
hello, guy!你先别急让我先急
这里我们仍需要先提出几个概念
静态类型Static Type在编译时已知的变量类型编译器根据静态类型进行方法的选择和类型检查。实际类型Actual Type在程序运行时确定的变量类型由对象的实际创建类型决定。
在上面的代码中Human 是静态类型也叫外观类型而 Man 和 Woman 则是实际类型也叫运行时类型。
我们用 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 方法里面的 26 和 31 是我们的方法调用
26: invokevirtual #34 // Method sayHello:(LStaticDispatch$Human;)V
......
31: invokevirtual #34 // Method sayHello:(LStaticDispatch$Human;)V反编译结果已经指明了尽管 invokevirtual 可以根据对象的实际类型进行动态分派但在静态分派的情况下编译器已经确定了要调用的方法因此不会进行动态分派而是直接调用编译时选择的方法。
即在静态分派的规则下方法的选择是基于参数的静态类型而不是实际运行时的类型。
这样也就不难理解了由于 man 和 woman 的静态类型都是 Human所以会调用 Human 的 sayHello() 方法。
但如果我们对原代码稍作修改
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 检查确认了 man 和 woman 强制转换为了对应的实际类型这样在 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()Man 和 Woman 类都是 Human 类的子类它们分别重写了 sayHello() 方法。我们首先创建了 Man 和 Woman 的实例对象 man 和 woman 并调用了相应的 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 指令并不是把常量池中方法的符号引用解析到直接引用上就结束了还会根据方法接收者的实际类型来选择方法版本。 单分派与多分派 方法的接收者与方法的参数统称为方法的宗量根据分派基于多少种宗量可以将分派划分为单分派和多分派两种
单分派指的是根据方法调用的接收者的类型来确定使用哪个方法实现。 在单分派中方法的选择仅仅依赖于接收者的类型不考虑方法参数的类型。 多分派指的是根据方法调用的接收者和参数的类型来确定使用哪个方法实现。 在多分派中方法的选择不仅依赖于接收者的类型还依赖于方法参数的类型。
对于 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 2023年9月6日; 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:stack1, locals1, args_size10: 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:stack3, locals2, args_size10: 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; // Testclass LambdaTest$Test of class LambdaTestpublic static final #74 #70 of #72; // Lookupclass 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:stack3, locals2, args_size10: 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 指令的运作过程
0: invokedynamic #7, 0 提示我们需要找到常量池中索引为 #7 的 InvokeDynamic 项。根据反编译结果我们可以找到这个项的描述符为 #0:showTestNumber:()LLambdaTest$Test;这也是 javap -v 预先添加的注释内容。接下来 #7 后面紧跟的 0 是我们需要寻找的解析引导方法即最后的 BootstrapMethods 中值为 0 的内容其引导方法描述符指向了 LambdaMetafactory.metafactory 方法。继续执行引导方法 #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表示一个方法类型对象指定要调用的方法的方法签名。 成功返回后将 CallSite 对象与目标方法进行绑定该方法有三个参数 #56是一个方法类型Ljava/lang/invoke/MethodType的参数表示被调用方法的参数类型和返回类型。#57是一个方法句柄Ljava/lang/invoke/MethodHandle的参数表示要调用的方法的句柄。#56是一个方法类型Ljava/lang/invoke/MethodType的参数表示被调用方法的参数类型和返回类型。 结合静态方法 LambdaMetafactory.metafactory 的第五个参数java/lang/invoke/MethodHandle方法句柄可知最终绑定到了 LambdaTest.lambda$main$0 方法这也就是 Lambda 表达式最终在 LambdaTest 中的 main 方法里生成的私有方法。最后由于 invokedynamic 指令在一开始调用了showTestNumber方法最终将返回的CallSite对象存储在局部变量表中等待调用。
通过这个过程invokedynamic 指令实现了对 LambdaTest 类中 showTestNumber 方法的动态调用。