在Java虚拟机上,函数调用是由五条命令完成的,它们是invokestatic,invokespecial,invokevirtual,invokeinterface和invokedynamic。它们分别用于不同的应用场景。在本章中,我们重点讲解前四条命令,invokedynamic将放在下一章介绍。
在Java语言中,函数调用大致可分为以下四种场景,分别对应着这四条invoke指令。
在接下来的小节中,我们将逐个讲解构造函数调用,成员函数调用和静态函数调用的场景。
这是一个简单的Java程序。在main()函数中创建了一个Example类的对象,并由局部变量ex引用该新创建的对象。
public class Example {
public static void main(String[] args) {
Example ex = new Example();
}
}
我们将上面的代码编译,并使用反编译查看其Java指令,如下所示。这段代码生成了main()函数和Example类的构造函数。表一和表二分别解释了这两个函数中指令的意义。我们将程序的执行过程总结如下。
表一、Example类构造函数中指令的意义。
指令的位移 | 指令助记符 | 指令的意义 |
---|---|---|
0 | aload_0 | 将索引为0的局部变量压入操作数栈。这个局部变量必须是一个对象引用。 |
1 | invokespecial #1 | 调用父类Object的构造函数。 |
4 | return | 函数返回。 |
表二、main函数中指令的意义。
指令的位移 | 指令助记符 | 指令的意义 |
---|---|---|
0 | new #2 | 从堆中创建一个新对象,新对象初始化为默认值,并将新对象的引用压入操作数栈。参数指向了在常量池中新对象的类型。 |
3 | dup | 复制操作数栈栈顶元素,并将其复制对象压入操作数栈。 |
4 | invokespecial #3 | 调用Example类的构造函数。"<init>"是Java编译器生成的,代表构造函数的函数名称。 |
7 | astore_1 | 弹出操作数栈栈顶元素,并将其赋值给索引为1的局部变量。这个变量必须是一个对象引用。 |
8 | return | 函数返回。 |
>javac Example.java
>javap -c Example
Compiled from "Example.java"
public class Example {
public Example();
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 #2 // class Example
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: return
}
图一也展示了示例的运行过程。值得注意的是,每一个函数调用都会在调用栈上创建一个新的Frame;每个新的Frame都会维护一个独立的操作数栈。
图一 构造函数调用示例运行图。
成员方法的调用是使用invokevirtual指令实现的。在下面的例子中,main()函数首先创建了一个Example类的对象ex,然后,调用成员方法increment()增加ex对象的成员变量value的值。最后,increment()方法返回当前ex.value的值。此外,本例还展示了Java虚拟机如何处理方法调用的返回值。成员方法increment()返回一个int类型的结果。
public class Example {
private int value = 0;
public int increment(int i) {
this.value += i;
return this.value;
}
public static void main(String[] args) {
Example ex = new Example();
int i = ex.increment(1);
}
}
我们将上面的代码编译,并使用反编译查看其Java指令,如下所示。这里展示了三个函数的代码,它们分别是Example类的构造函数,increment()成员方法和main()函数。因为Example类的构造函数与第一个例子非常类似(见表一),所以,这里我们省略Example类构造函数的解释。在阅读下面代码时,请读者留意三点。其一,在成员函数increment()内部,this存放在第一个局部变量(其索引值为0)中,所以,在实现中,increment()方法实际上是int increment(Example this, int i)。其二,获取和设置成员变量是由指令getfield和putfield完成的。它们的参数指向了常量池中的成员变量引用类型(CONSTANT_Fieldref),并且操作的对象是从操作数栈中弹出的。其三,当一个方法返回类型是boolean, byte, char, short或者int时,指令ireturn会从当前Frame的操作数栈中弹出栈顶元素,并将其压入调用者的操作数栈的栈顶。因此,调用者可以从它的调用栈的栈顶获得函数的返回值。指令lreturn、freturn、dreturn和areturn分别处理返回类型是long、float、double和对象引用的情况。
表三、increment()函数中指令的意义。
指令的位移 | 指令助记符 | 指令的意义 |
---|---|---|
0 | aload_0 | 将索引为0的局部变量压入操作数栈。这个局部变量必须是一个对象引用。 |
1 | dup | 复制操作数栈栈顶元素,并将其复制对象压入操作数栈。 |
2 | getfield #2 | 从操作数栈中弹出一个对象,获取该对象的成员变量,并将其压入操作数栈。成员变量由参数指定。 |
5 | iload_1 | 将本地索引为1的局部变量(即变量i)压入操作数栈。 |
6 | iadd | 从操作数栈顶部弹出两个元素,并累加这两个元素,最后将结果压回操作数栈。 |
7 | putfield #2 | 从操作数栈中弹出一个值Value和一个对象,并将该值赋值给这个对象的成员变量。成员变量由参数指定。 |
10 | aload_0 | 将索引为0的局部变量压入操作数栈。这个局部变量必须是一个对象引用。 |
11 | getfield #2 | 从操作数栈中弹出一个对象,获取该对象的成员变量,并将其压回操作数栈。成员变量由参数指定。 |
14 | ireturn | 弹出当前Frame的操作数栈的栈顶元数,并将其压入调用者的操作数栈中。 |
表四、main()函数中指令的意义。
指令的位移 | 指令助记符 | 指令的意义 |
---|---|---|
0 | new #3 | 从堆中创建一个新对象,新对象初始化为默认值,并将新对象的引用压入操作数栈。参数指向了在常量池中新对象的类型。 |
3 | dup | 复制操作数栈的栈顶元素,并将其压入操作数栈。 |
4 | invokespecial #4 | 调用Example类的构造函数。 |
7 | astore_1 | 从操作数栈中弹出一个元素,并将其赋值给索引为1的本地变量(第二个本地变量,即变量ex)。 |
8 | aload_1 | 将索引为1的本地变量(即变量ex)压入操作数栈。 |
9 | iconst_1 | 将1压入操作数栈。 |
10 | invokevirtual #5 | 调用increment()成员方法。 |
13 | istore_2 | 从操作数栈中弹出栈顶元素,并将其赋值给索引为2的本地变量(即变量i)。 |
14 | return | 函数返回。 |
>javac Example.java
>javap -c Example
Compiled from "Example.java"
public class Example {
public Example();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_0
6: putfield #2 // Field value:I
9: return
public int increment(int);
Code:
0: aload_0
1: dup
2: getfield #2 // Field value:I
5: iload_1
6: iadd
7: putfield #2 // Field value:I
10: aload_0
11: getfield #2 // Field value:I
14: ireturn
public static void main(java.lang.String[]);
Code:
0: new #3 // class Example
3: dup
4: invokespecial #4 // Method "<init>":()V
7: astore_1
8: aload_1
9: iconst_1
10: invokevirtual #5 // Method increment:(I)I
13: istore_2
14: return
}
图二也展示了示例的运行过程。值得注意的是,每调用一个成员方法都会在调用栈上创建一个新的Frame;每个新的Frame都会维护一个独立的操作数栈。
图二 成员方法调用示例运行图。
静态方法调用是由指令invokestatic完成的。从调用的过程来看,Java虚拟机使用操作数栈来为静态方法传递参数。在下面的例子中,Example类定义了两个静态方法increment()和main()。在main()函数中,调用increment()方法。
public class Example {
public static int increment(int i) {
return i+1;
}
public static void main(String[] args) {
int i = increment(1);
}
}
我们将上面的代码编译,并使用反编译查看其Java指令,如下所示。参数传递的过程与调用成员函数类似,唯一的区别是调用静态方法不需要创建或者传递Example类的对象。我们将每条指令解释如下。
表五、increment()方法中指令的意义。
指令的位移 | 指令助记符 | 指令的意义 |
---|---|---|
0 | iconst_1 | 将常数1压入操作数栈。 |
1 | invokestatic #2 | 函数调用;待调用的函数由参数指定。 |
4 | istore_1 | 弹出操作数栈栈顶元素,将其存入索引为1的局部变量中。 |
5 | return | 函数返回。 |
表六、main函数中指令的意义。
指令的位移 | 指令助记符 | 指令的意义 |
---|---|---|
0 | iload_0 | 将索引为0的局部变量压入操作数栈中。 |
1 | iconst_1 | 将常数1压入操作数栈。 |
4 | iadd | 从操作数栈中弹出两个元素,并将这两个元素之和压回操作数栈。 |
5 | ireturn | 从当前Frame的操作数栈中弹出栈顶元素,并将其压入调用者的操作数栈中。 |
>javac Example.java
>javap -c Example
Compiled from "Example.java"
public class Example {
public Example();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static int increment(int);
Code:
0: iload_0
1: iconst_1
2: iadd
3: ireturn
public static void main(java.lang.String[]);
Code:
0: iconst_1
1: invokestatic #2 // Method increment:(I)I
4: istore_1
5: return
}
图三也展示了示例的运行过程。
图三 静态方法调用示例运行图。
本章通过三个例子分别讲解了构造函数调用、成员方法调用和静态方法调用的过程。这三种函数调用是由invokespecial,invokevirtual和invokestatic指令实现的。我们将在下一章介绍invokedynamic指令,它是Java虚拟机支持动态扩展的关键内容。同时,为了提高性能,Java设计者使用了invokedynamic指令来实现Lambda的函数调用。
注册用户登陆后可留言