04_jvm_08_instruction_2

第五十四章 Java虚拟机的解释器与指令集之函数调用

1 概述

在Java虚拟机上,函数调用是由五条命令完成的,它们是invokestatic,invokespecial,invokevirtual,invokeinterface和invokedynamic。它们分别用于不同的应用场景。在本章中,我们重点讲解前四条命令,invokedynamic将放在下一章介绍。

在Java语言中,函数调用大致可分为以下四种场景,分别对应着这四条invoke指令。

  1. invokestatic:调用类/接口的静态函数(Static Method)。在函数调用的过程中,Java解释器会在调用栈中创建一个新的Frame,对应于接下来将要执行的静态函数。该函数的参数依次存放在新Frame的局部变量(Local Variables)中。第一个参数是索引为0的局部变量,第二个参数是索引为1的局部变量,以此类推。在设置好局部变量之后,Java解释器会将当前线程的pc字段指向该静态函数的第一条指令。如果该静态函数声明为synchronized,则在运行该函数之前需执行monitorenter指令。在函数退出前会执行monitorexit指令。这两个指令用于获取/释放对应对象的monitor
  2. invokevirtual:调用类的成员函数(Member Method)。调用成员函数的过程与调用静态函数的过程非常相似,但是它们有两个显著的区别。其一,查找待调用的函数的过程不同。待调用的函数由调用对象的类型决定(多态特性)。其二,在新创建的Frame中,索引为0的局部变量是当前调用对象(或者是this变量),索引为1的局部变量是方法的第一个参数,索引为2的局部变量是方法的第二个参数,以此类推。
  3. invokeinterface:调用接口的成员函数。从逻辑上看,invokeinterface与invokevirtual是非常相似的;invokeinterface对应着调用接口函数。但是,在Java虚拟机的实现中,因为Java不支持多继承,所以,一个类只有一个直接父类,而允许有多个直接父接口,因此,Java虚拟机可以采用一些优化算法加速类成员函数的调用,而这些优化算法无法应用在接口成员函数的调用上。因此,Java指令集为这两种函数调用分别设计了独立的指令。
  4. invokespecial:适用于调用构造函数和使用关键字super调用函数。与invokevirtual不同的是,invokespecial使用的是类的类型信息来选择函数,而invokevirtual是使用对象的类型信息选择函数。

在接下来的小节中,我们将逐个讲解构造函数调用,成员函数调用和静态函数调用的场景。

2 构造函数调用示例

这是一个简单的Java程序。在main()函数中创建了一个Example类的对象,并由局部变量ex引用该新创建的对象。

public class Example {
    public static void main(String[] args) {
        Example ex = new Example();
    }
}

我们将上面的代码编译,并使用反编译查看其Java指令,如下所示。这段代码生成了main()函数和Example类的构造函数。表一和表二分别解释了这两个函数中指令的意义。我们将程序的执行过程总结如下。

  1. 在main()函数中,该程序首先使用new指令在堆中创建一个新的对象,并将其内部的成员设置为默认值。
  2. 使用dup指令复制了一份栈顶元素,这是因为程序需要将这个元素传递给构造函数,而构造函数并不返回任何数据。所以,如果此时不复制栈顶元素的话,在调用完构造函数后,main()函数将无法访问新创建的对象了。
  3. 使用invokespecial指令调用Example类的构造函数。在这个过程中,程序会创建一个新的Frame和一个新的操作数栈,然后从老的操作数栈中弹出栈顶元素,并将其赋值给索引为0的本地变量。
  4. aload_0指令将索引为0的本地变量压入新的操作数栈中,并继续使用invokespecial指令调用Object类的构造函数。这个索引为0的本地变量就是在main()函数中使用dup指令复制出的元素。
  5. Example类的构造函数运行完毕。Java虚拟机清理新创建的Frame和操作数栈。
  6. astore_1将当前的操作数栈的栈顶元素赋值给索引为1的本地变量(即变量ex)。因为main()函数有一个输入参数args;args是本地索引为0的本地变量。所以,本地变量ex排在第二位。
  7. 程序结束。

表一、Example类构造函数中指令的意义。

指令的位移指令助记符指令的意义
0aload_0将索引为0的局部变量压入操作数栈。这个局部变量必须是一个对象引用。
1invokespecial #1调用父类Object的构造函数。
4return函数返回。

表二、main函数中指令的意义。

指令的位移指令助记符指令的意义
0new #2从堆中创建一个新对象,新对象初始化为默认值,并将新对象的引用压入操作数栈。参数指向了在常量池中新对象的类型。
3dup复制操作数栈栈顶元素,并将其复制对象压入操作数栈。
4invokespecial #3调用Example类的构造函数。"<init>"是Java编译器生成的,代表构造函数的函数名称。
7astore_1弹出操作数栈栈顶元素,并将其赋值给索引为1的局部变量。这个变量必须是一个对象引用。
8return函数返回。
>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都会维护一个独立的操作数栈。

图一 构造函数调用示例运行图

图一 构造函数调用示例运行图。

3 成员方法调用示例

成员方法的调用是使用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()函数中指令的意义。

指令的位移指令助记符指令的意义
0aload_0将索引为0的局部变量压入操作数栈。这个局部变量必须是一个对象引用。
1dup复制操作数栈栈顶元素,并将其复制对象压入操作数栈。
2getfield #2从操作数栈中弹出一个对象,获取该对象的成员变量,并将其压入操作数栈。成员变量由参数指定。
5iload_1将本地索引为1的局部变量(即变量i)压入操作数栈。
6iadd从操作数栈顶部弹出两个元素,并累加这两个元素,最后将结果压回操作数栈。
7putfield #2从操作数栈中弹出一个值Value和一个对象,并将该值赋值给这个对象的成员变量。成员变量由参数指定。
10aload_0将索引为0的局部变量压入操作数栈。这个局部变量必须是一个对象引用。
11getfield #2从操作数栈中弹出一个对象,获取该对象的成员变量,并将其压回操作数栈。成员变量由参数指定。
14ireturn弹出当前Frame的操作数栈的栈顶元数,并将其压入调用者的操作数栈中。

表四、main()函数中指令的意义。

指令的位移指令助记符指令的意义
0new #3从堆中创建一个新对象,新对象初始化为默认值,并将新对象的引用压入操作数栈。参数指向了在常量池中新对象的类型。
3dup复制操作数栈的栈顶元素,并将其压入操作数栈。
4invokespecial #4调用Example类的构造函数。
7astore_1从操作数栈中弹出一个元素,并将其赋值给索引为1的本地变量(第二个本地变量,即变量ex)。
8aload_1将索引为1的本地变量(即变量ex)压入操作数栈。
9iconst_1将1压入操作数栈。
10invokevirtual #5调用increment()成员方法。
13istore_2从操作数栈中弹出栈顶元素,并将其赋值给索引为2的本地变量(即变量i)。
14return函数返回。
>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都会维护一个独立的操作数栈。

图二 成员方法调用示例运行图

图二 成员方法调用示例运行图。

4 静态方法调用示例

静态方法调用是由指令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()方法中指令的意义。

指令的位移指令助记符指令的意义
0iconst_1将常数1压入操作数栈。
1invokestatic #2函数调用;待调用的函数由参数指定。
4istore_1弹出操作数栈栈顶元素,将其存入索引为1的局部变量中。
5return函数返回。

表六、main函数中指令的意义。

指令的位移指令助记符指令的意义
0iload_0将索引为0的局部变量压入操作数栈中。
1iconst_1将常数1压入操作数栈。
4iadd从操作数栈中弹出两个元素,并将这两个元素之和压回操作数栈。
5ireturn从当前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
}

图三也展示了示例的运行过程。

图三 静态方法调用示例运行图

图三 静态方法调用示例运行图。

5 总结

本章通过三个例子分别讲解了构造函数调用、成员方法调用和静态方法调用的过程。这三种函数调用是由invokespecial,invokevirtual和invokestatic指令实现的。我们将在下一章介绍invokedynamic指令,它是Java虚拟机支持动态扩展的关键内容。同时,为了提高性能,Java设计者使用了invokedynamic指令来实现Lambda的函数调用。

 

上一章
下一章

注册用户登陆后可留言

Copyright  2019 Little Waterdrop, LLC. All Rights Reserved.