文章目录
介绍-方法调用指令
首先要清楚的是,在Java中,有关字节码文件的编译过程不包含传统程序语言编译的连接步骤,一切方法调用在字节码文件中只是符号引用,而不是在方法在实际运行时内存布局中的入口地址,也就是直接引用。
在上一篇文章《探索JVM类加载机制》中,分析了在类加载的解析阶段,会将一部分的符号引用转化为直接引用,这种解析能够成立的前提条件就是:方法在程序真正运行之前就有一个可以确定的调用版本,并且这个方法的调用版本在运行期间是不可变的,也就是该方法调用具有唯一的目标方法。从程序的角度而言,调用目标在程序代码写好,编译器进行编译的那一刻就已经确定下来了,这类方法的调用称之为解析。
在《Java虚拟机规范》中有以下五种方法调用字节码指令:
- invokestatic:用于调用静态方法。
- invokespecial:用于调用实例构造器
<init>()
,私有方法和父类中的方法。 - invokeinterface:用于调用接口方法,会在运行时确定一个实现该接口的对象。
- invokedynamic:在运行时动态解析初调用点限定符所引用的方法,然后在执行该方法。
- invokevirtual:用于调用所有的虚方法。
下面将会演示以上五种指令的实际应用,在介绍之前,需要借助Java开发工具包(JDK)中提供的一个命令行工具 javap,用于反编译Java字节码,以便深入了解和分析已编译的Java类文件。指令如下,有关其详细介绍内容,读者可从网上查阅,在此不赘述。
sh
# -verbose 会显示与指定类相关的字节码指令、常量池、方法、字段和其他类信息。
javap -verbose [字节码文件的路径]
1.invokestatic
静态方法测试类
java
public class StaticMethodCallInstruction {
public static void main(String[] args) {
int num = getNum(1); // 根据下方反编译结果可知,此处会生成一个invokestatic指令用于调用Method:getNum:(I)I
System.out.println(num);
}
// 静态方法,该方法编译后简单名称为getNum,描述符为(I)I,根据上一篇文章所述,在进行方法搜索时会匹配到该方法。
public static int getNum(int i){
return i;
}
}
反编译字节码
shell
# 执行命令
javap -verbose /target/classes/com/mytest/project/method/call/StaticMethodCallInstruction.class
# 以下仅展示主要内容,并会简单介绍一下相关内容,其他与本次案例无关的内容将省略
# 常量池,表类型结构
Constant pool:
#2 = Methodref #5.#27 // com/mytest/project/method/call/StaticMethodCallInstruction.getNum:(I)I
#3 = Fieldref #28.#29 // java/lang/System.out:Ljava/io/PrintStream;
#4 = Methodref #30.#31 // java/io/PrintStream.println:(I)V
#5 = Class #32 // com/mytest/project/method/call/StaticMethodCallInstruction
#27 = NameAndType #21:#22 // getNum:(I)I
#28 = Class #34 // java/lang/System
#29 = NameAndType #35:#36 // out:Ljava/io/PrintStream;
#30 = Class #37 // java/io/PrintStream
#31 = NameAndType #38:#39 // println:(I)V
#32 = Utf8 com/mytest/project/method/call/StaticMethodCallInstruction
# main方法
public static void main(java.lang.String[]);
# 描述符
descriptor: ([Ljava/lang/String;)V
# 访问标记
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
# 方法属性表
Code:
# 操作数栈深度,局部变量表的大小,方法参数表的大小
stack=2, locals=2, args_size=1
0: iconst_1
1: invokestatic #2 // Method getNum:(I)I
4: istore_1
5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
8: iload_1
9: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
12: return
# 行号映射表(源码与code属性中的索引)
LineNumberTable:
line 12: 0
line 13: 5
line 14: 12
# 局部变量表
LocalVariableTable:
Start Length Slot Name Signature
0 13 0 args [Ljava/lang/String;
5 8 1 num I
# 方法参数表
MethodParameters:
Name Flags
args
# getNum方法
public static int getNum(int);
descriptor: (I)I
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: iload_0
1: ireturn
LineNumberTable:
line 17: 0
LocalVariableTable:
Start Length Slot Name Signature
0 2 0 i I
MethodParameters:
Name Flags
i
2.invokespecial
特殊方法测试类
java
//父类
public class ParentMethod {
public void parentMethod(){
return;
}
}
// 子类
public class SpecialMethodCallInstruction extends ParentMethod {
// 无参构造器
public SpecialMethodCallInstruction(){}
public void demo() {
// 调用实例构造器方法
new SpecialMethodCallInstruction(); // class com/mytest/project/method/call/SpecialMethodCallInstruction
// 调用私有方法
test(); // Method test:()V
// 调用父类方法
super.parentMethod(); // Method com/mytest/project/method/ParentMethod.parentMethod:()V
// 该方法在编译时会被认为时虚方法,需要显示调用父类方法,才会执行invokespecial
parentMethod(); // Method parentMethod:()V
}
// 私有方法
private void test(){
return;
}
}
反编译字节码
shell
public void demo();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: new #2 // class com/mytest/project/method/call/SpecialMethodCallInstruction
3: dup
4: invokespecial #3 // Method "<init>":()V
7: pop
8: aload_0
9: invokespecial #4 // Method test:()V
12: aload_0
13: invokespecial #5 // Method com/mytest/project/method/ParentMethod.parentMethod:()V
16: aload_0
17: invokevirtual #6 // Method parentMethod:()V
20: return
LineNumberTable:
line 18: 0
line 20: 8
line 22: 12
line 24: 16
line 25: 20
LocalVariableTable:
Start Length Slot Name Signature
0 21 0 this Lcom/mytest/project/method/call/SpecialMethodCallInstruction;
3.invokeinterface
接口方法测试类
java
public class InterfaceMethodCallInstruction implements InterfaceMethod {
public void demo(){
InterfaceMethod interfaceMethod = new InterfaceMethodCallInstruction();
// 调用接口方法
interfaceMethod.iMethod(); // com/mytest/project/method/call/intf/InterfaceMethod.iMethod:()V
}
@Override
public void iMethod() {
return;
}
}
反编译字节码
shell
public void demo();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class com/mytest/project/method/call/InterfaceMethodCallInstruction
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokeinterface #4, 1 // InterfaceMethod com/mytest/project/method/call/intf/InterfaceMethod.iMethod:()V
14: return
LineNumberTable:
line 14: 0
line 15: 8
line 16: 14
LocalVariableTable:
Start Length Slot Name Signature
0 15 0 this Lcom/mytest/project/method/call/InterfaceMethodCallInstruction;
8 7 1 interfaceMethod Lcom/mytest/project/method/call/intf/InterfaceMethod;
4.invokedynamic
invokedynamic 指令用于在运行时动态确定要调用的方法。与传统的 invokevirtual、invokeinterface 等指令不同,invokedynamic 指令将查找目标方法的决定权从虚拟机转嫁到具体用户代码之中,每一处含有invokedynamic指令的位置都称做"动态调用点"(Dynamic Call Site),这条指令的第一个参数不再是代表方法符号引用的CONSTANT_Methodref_info常量,而是变为JDK 1.7新加入的CONSTANT_InvokeDynamic_info常量,从这个新常量中可以得到3项信息:引导方法(Bootstrap Method,此方法存放在新增的BootstrapMethods属性中)、方法类型(MethodType)和名称。
引导方法是 invokedynamic 指令的核心部分,它负责在运行时生成和链接要调用的方法。引导方法是一个普通的Java方法,但它具有特殊的签名和参数。引导方法的参数包括:
- 一个查找器(Lookup)对象,用于查找和创建方法句柄、构造函数句柄等;
- 一个名称(Name)对象,表示要调用的方法的名称;
- 一个方法类型(MethodType)对象,表示要调用的方法的参数类型和返回类型;
- 可变数量的静态参数,这些参数是传递给引导方法的附加信息,用于生成和链接目标方法。
引导方法的主要任务是根据这些参数动态生成和链接目标方法。生成目标方法的过程可以是通过反射API查找已存在的方法,也可以是动态生成新的方法(例如,使用ASM库或Java编译器API)。链接目标方法的过程是将生成的目标方法与invokedynamic指令关联起来,以便在运行时正确调用。
当JVM执行到 invokedynamic 指令时,它会首先查找与该指令关联的引导方法。引导方法根据传入的参数动态生成和链接目标方法,并返回一个称为"调用站点"(CallSite)的对象。调用站点对象封装了目标方法的所有信息,包括方法句柄、参数类型和返回类型等。
一旦调用站点对象被创建并返回给 invokedynamic 指令,JVM就会将该指令与调用站点对象关联起来。在后续的执行过程中,当再次遇到相同的invokedynamic指令时,JVM会直接通过调用站点对象调用目标方法,而无需再次执行引导方法。这种机制可以显著提高动态方法调用的性能。
以上说明内容参考链接:https://blog.csdn.net/li371518473/article/details/136646069
动态调用方法测试类
java
public class DynamicMethodCallInstruction {
public void demo(){
Thread thread = new Thread(() -> {
int i = 0;
i++;
});
thread.run();
}
}
反编译字节码
shell
public void demo();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=3, locals=2, args_size=1
0: new #2 // class java/lang/Thread
3: dup
4: invokedynamic #3, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
// 其中#0表示#BootstrapMethods属性表的第0项
9: invokespecial #4 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
12: astore_1
13: aload_1
14: invokevirtual #5 // Method java/lang/Thread.run:()V
17: return
LineNumberTable:
line 14: 0
line 18: 13
line 19: 17
LocalVariableTable:
Start Length Slot Name Signature
0 18 0 this Lcom/mytest/project/method/call/DynamicMethodCallInstruction;
13 5 1 thread Ljava/lang/Thread;
# BootstrapMethods属性
BootstrapMethods:
# LambdaMetafactory.metafactory会生成一个实现了函数式接口的匿名内部类,并实例化,然后委托给MethodHandle返回一个CallSite对象。该CallSite类型的对象实现对目标方法也就是run()的调用
0: #26 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:
#27 ()V
#28 REF_invokeStatic com/mytest/project/method/call/DynamicMethodCallInstruction.lambda$demo$0:()V
#27 ()V
5.invokevirtual
在Java 语言能在解析阶段中唯一确定其调用版本的方法有静态方法、私有方法、实例构造器、父类方法以及被final
修饰的方法 ,这5种方法会在类加载的时候就可以把符号引用解析为该方法的直接引用(入口地址)。这些方法统称为"非虚方法 ",相反,其他方法被称之为"虚方法"。
所以通常认为,虚方法是指在类加载阶段(这里指的是解析阶段)都不能确定方法调用的直接引用,而只有在运行时才能确定的方法。
由于历史设计的原因,被 final
修饰的实例方法是使用 invokevirtual 指令来调用的,但是因为其无法被覆盖,没有其他版本的可能,所以方法接受者也不需要进行多态选择,又或者说多态选择的结果是唯一的。所以,在《Java语言规范》中明确定义了被 final
修饰的方法是一种非虚方法。
虚方法测试类
java
public class VirtualMethodCallInstruction {
public void demo(){
// 调用final修饰的实例方法,实际上是非虚方法,但是指令为invokevirtual
fMethod();
// 调用final修饰的类方法,指令为invokestatic
sfMethod();
}
public final void fMethod(){
return;
}
public static final void sfMethod(){
return;
}
}
反编译字节码
shell
public void demo();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokevirtual #2 // Method fMethod:()V
4: invokestatic #3 // Method sfMethod:()V
7: return
LineNumberTable:
line 13: 0
line 15: 4
line 16: 7
LocalVariableTable:
Start Length Slot Name Signature
0 8 0 this Lcom/mytest/project/method/call/VirtualMethodCallInstruction;