接下看我们具体看一个实际的代码示例:
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.class
Last modified 2023年9月6日; size 276 bytes
SHA-256 checksum 4064a19d96fe4d72c9d780ef819e1e937b120c31b37482e0b74c70e37c2a5601
Compiled from "Test.java"
public class Test
minor version: 0
major version: 61
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #7 // Test
super_class: #2 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Class #8 // Test
#8 = Utf8 Test
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 Test.java
{
public Test();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: bipush 114
2: istore_1
3: sipush 514
6: istore_2
7: iload_1
8: iload_2
9: iadd
10: istore_3
11: return
LineNumberTable:
line 3: 0
line 4: 3
line 5: 7
line 6: 11
}
SourceFile: "Test.java"
我们专注于下列信息:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: bipush 114
2: istore_1
3: sipush 514
6: istore_2
7: iload_1
8: iload_2
9: iadd
10: istore_3
11: return
其中:
public static void main(java.lang.String[])
:表示这是一个公共的静态方法,方法名为 main
,它接受一个 java.lang.String
类型的数组作为参数。descriptor: ([Ljava/lang/String;)V
:说明了方法的描述符,其中 ([Ljava/lang/String;)
表示参数类型为 java.lang.String
类型的数组,V
表示方法的返回类型为 void
。flags: (0x0009) ACC_PUBLIC, ACC_STATIC
:这是方法的标志,其中 ACC_PUBLIC
表示该方法是公共的,ACC_STATIC
表示该方法是静态的。我们针对其中的 main
入口代码 Code
展示解释器的执行过程,其中:
stack=2, locals=4, args_size=1
提示我们这段代码需要深度为 2 的操作数栈、 4 个变量槽的局部变量空间和 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
虚拟机中揭示了多态性的体现,特别是在方法的重载和重写方面:
下面我们就来揭示 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!
你先别急,让我先急 🤡
这里我们仍需要先提出几个概念:
在上面的代码中,Human
是静态类型(也叫外观类型),而 Man
和 Woman
则是实际类型(也叫运行时类型)。
我们用 javap
查看反编译结果:
Compiled from "StaticDispatch.java"
public class StaticDispatch {
public StaticDispatch();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void sayHello(StaticDispatch$Human);
Code:
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #13 // String hello,guy!
5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
public void sayHello(StaticDispatch$Man);
Code:
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #21 // String hello,gentleman!
5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
public void sayHello(StaticDispatch$Woman);
Code:
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #23 // String hello,lady!
5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
public static void main(java.lang.String[]);
Code:
0: new #25 // class StaticDispatch$Man
3: dup
4: invokespecial #27 // Method StaticDispatch$Man."<init>":()V
7: astore_1
8: new #28 // class StaticDispatch$Woman
11: dup
12: invokespecial #30 // Method StaticDispatch$Woman."<init>":()V
15: astore_2
16: new #31 // class StaticDispatch
19: dup
20: invokespecial #33 // Method "<init>":()V
23: astore_3
24: aload_3
25: aload_1
26: invokevirtual #34 // Method sayHello:(LStaticDispatch$Human;)V
29: aload_3
30: aload_2
31: invokevirtual #34 // Method sayHello:(LStaticDispatch$Human;)V
34: return
}
可以明显地看到 main
方法里面的 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 {
@Override
protected void sayHello() {
System.out.println("man say hello");
}
}
static class Woman extends Human {
@Override
protected void sayHello() {
System.out.println("woman say hello");
}
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
man.sayHello();
woman.sayHello();
man = new Woman();
man.sayHello();
}
}
在上述代码中,Human
是一个抽象类,其中声明了一个抽象方法 sayHello()
,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_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #7 // class DynamicDispatch$Man
3: dup
4: invokespecial #9 // Method DynamicDispatch$Man."<init>":()V
7: astore_1
8: new #10 // class DynamicDispatch$Woman
11: dup
12: invokespecial #12 // Method DynamicDispatch$Woman."<init>":()V
15: astore_2
16: aload_1
17: invokevirtual #13 // Method DynamicDispatch$Human.sayHello:()V
20: aload_2
21: invokevirtual #13 // Method DynamicDispatch$Human.sayHello:()V
24: new #10 // class DynamicDispatch$Woman
27: dup
28: invokespecial #12 // Method DynamicDispatch$Woman."<init>":()V
31: astore_1
32: aload_1
33: invokevirtual #13 // Method DynamicDispatch$Human.sayHello:()V
36: return
}
在 main
方法中:
Human man = new Man();
Human woman = new Woman();
对应字节码指令为:
0: new #7 // class DynamicDispatch$Man
3: dup
4: invokespecial #9 // Method DynamicDispatch$Man."<init>":()V
7: astore_1
8: new #10 // class DynamicDispatch$Woman
11: dup
12: invokespecial #12 // Method DynamicDispatch$Woman."<init>":()V
15: astore_2
而调用方法:
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
指令本身入手,其运行时解析过程大致分为以下几步:
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.class
Last modified 2023年9月6日; size 1445 bytes
SHA-256 checksum 3a71d05fe531173bda2fd05e7b9a5a12dc0fd040047f87a59add471744a6a2be
Compiled from "LambdaTest.java"
public class LambdaTest
minor version: 0
major version: 61
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #38 // LambdaTest
super_class: #2 // java/lang/Object
interfaces: 0, fields: 0, methods: 3, attributes: 4
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = InvokeDynamic #0:#8 // #0:showTestNumber:()LLambdaTest$Test;
#8 = NameAndType #9:#10 // showTestNumber:()LLambdaTest$Test;
#9 = Utf8 showTestNumber
#10 = Utf8 ()LLambdaTest$Test;
#11 = Fieldref #12.#13 // java/lang/System.out:Ljava/io/PrintStream;
#12 = Class #14 // java/lang/System
#13 = NameAndType #15:#16 // out:Ljava/io/PrintStream;
#14 = Utf8 java/lang/System
#15 = Utf8 out
#16 = Utf8 Ljava/io/PrintStream;
#17 = Integer 114514
#18 = Methodref #19.#20 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
#19 = Class #21 // java/lang/Integer
#20 = NameAndType #22:#23 // valueOf:(I)Ljava/lang/Integer;
#21 = Utf8 java/lang/Integer
#22 = Utf8 valueOf
#23 = Utf8 (I)Ljava/lang/Integer;
#24 = InterfaceMethodref #25.#26 // LambdaTest$Test.showTestNumber:(Ljava/lang/Integer;)Ljava/lang/String;
#25 = Class #27 // LambdaTest$Test
#26 = NameAndType #9:#28 // showTestNumber:(Ljava/lang/Integer;)Ljava/lang/String;
#27 = Utf8 LambdaTest$Test
#28 = Utf8 (Ljava/lang/Integer;)Ljava/lang/String;
#29 = Methodref #30.#31 // java/io/PrintStream.println:(Ljava/lang/String;)V
#30 = Class #32 // java/io/PrintStream
#31 = NameAndType #33:#34 // println:(Ljava/lang/String;)V
#32 = Utf8 java/io/PrintStream
#33 = Utf8 println
#34 = Utf8 (Ljava/lang/String;)V
#35 = InvokeDynamic #1:#36 // #1:makeConcatWithConstants:(Ljava/lang/Integer;)Ljava/lang/String;
#36 = NameAndType #37:#28 // makeConcatWithConstants:(Ljava/lang/Integer;)Ljava/lang/String;
#37 = Utf8 makeConcatWithConstants
#38 = Class #39 // LambdaTest
#39 = Utf8 LambdaTest
#40 = Utf8 Code
#41 = Utf8 LineNumberTable
#42 = Utf8 main
#43 = Utf8 ([Ljava/lang/String;)V
#44 = Utf8 lambda$main$0
#45 = Utf8 SourceFile
#46 = Utf8 LambdaTest.java
#47 = Utf8 NestMembers
#48 = Utf8 BootstrapMethods
#49 = MethodHandle 6:#50 // REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#50 = Methodref #51.#52 // java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#51 = Class #53 // java/lang/invoke/LambdaMetafactory
#52 = NameAndType #54:#55 // metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#53 = Utf8 java/lang/invoke/LambdaMetafactory
#54 = Utf8 metafactory
#55 = Utf8 (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#56 = MethodType #28 // (Ljava/lang/Integer;)Ljava/lang/String;
#57 = MethodHandle 6:#58 // REF_invokeStatic LambdaTest.lambda$main$0:(Ljava/lang/Integer;)Ljava/lang/String;
#58 = Methodref #38.#59 // LambdaTest.lambda$main$0:(Ljava/lang/Integer;)Ljava/lang/String;
#59 = NameAndType #44:#28 // lambda$main$0:(Ljava/lang/Integer;)Ljava/lang/String;
#60 = MethodHandle 6:#61 // REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
#61 = Methodref #62.#63 // java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
#62 = Class #64 // java/lang/invoke/StringConcatFactory
#63 = NameAndType #37:#65 // makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
#64 = Utf8 java/lang/invoke/StringConcatFactory
#65 = Utf8 (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
#66 = String #67 // Test number is \u0001
#67 = Utf8 Test number is \u0001
#68 = Utf8 InnerClasses
#69 = Utf8 Test
#70 = Class #71 // java/lang/invoke/MethodHandles$Lookup
#71 = Utf8 java/lang/invoke/MethodHandles$Lookup
#72 = Class #73 // java/lang/invoke/MethodHandles
#73 = Utf8 java/lang/invoke/MethodHandles
#74 = Utf8 Lookup
{
public LambdaTest();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1
0: invokedynamic #7, 0 // InvokeDynamic #0:showTestNumber:()LLambdaTest$Test;
5: astore_1
6: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream;
9: aload_1
10: ldc #17 // int 114514
12: invokestatic #18 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
15: invokeinterface #24, 2 // InterfaceMethod LambdaTest$Test.showTestNumber:(Ljava/lang/Integer;)Ljava/lang/String;
20: invokevirtual #29 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
23: return
LineNumberTable:
line 8: 0
line 9: 6
line 10: 23
}
SourceFile: "LambdaTest.java"
NestMembers:
LambdaTest$Test
BootstrapMethods:
0: #49 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
#56 (Ljava/lang/Integer;)Ljava/lang/String;
#57 REF_invokeStatic LambdaTest.lambda$main$0:(Ljava/lang/Integer;)Ljava/lang/String;
#56 (Ljava/lang/Integer;)Ljava/lang/String;
1: #60 REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
Method arguments:
#66 Test number is \u0001
InnerClasses:
public static #69= #25 of #38; // Test=class LambdaTest$Test of class LambdaTest
public static final #74= #70 of #72; // Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
我们还是重点灌注 main
方法里面的内容:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1
0: invokedynamic #7, 0 // InvokeDynamic #0:showTestNumber:()LLambdaTest$Test;
5: astore_1
6: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream;
9: aload_1
10: ldc #17 // int 114514
12: invokestatic #18 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
15: invokeinterface #24, 2 // InterfaceMethod LambdaTest$Test.showTestNumber:(Ljava/lang/Integer;)Ljava/lang/String;
20: invokevirtual #29 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
23: return
LineNumberTable:
line 8: 0
line 9: 6
line 10: 23
}
其中:
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
)的参数,表示被调用方法的参数类型和返回类型。invokedynamic
指令在一开始调用了showTestNumber
方法,最终将返回的CallSite
对象存储在局部变量表中等待调用。通过这个过程,invokedynamic
指令实现了对 LambdaTest
类中 showTestNumber
方法的动态调用。