jvm dup 指令的用例

Use cases of jvm dup instruction

Java字节码指令集提供various forms of dup instruction。我无法理解这些说明和 swap 说明的用处。 哪些 java 代码在编译时会使用这些指令生成字节码?

我不知道javac什么时候用到它,但是我们在生成代码的时候经常用到DUP和SWAP。例如,如果你正在做相当于

x.setCharm(y);
x.setSpin(z);

然后你会加载 x 并立即 DUP 它,因为调用第一个方法会把它从堆栈中取出,你想使用它两次。

SWAP 在您执行类似

的操作时会派上用场
y = x.getCharm();
z.setCharm(y);

第一条指令将 y 留在堆栈顶部,然后堆栈 z 和 SWAP,因此您现在堆栈上有正确的值来调用第二条指令。

dup 的变体可以出现在普通 Java 代码中。

例如如 中所述,对象实例化通常使用 dup,因为 new Object() 被编译为

new #n              // n referencing Class java.lang.Object in the pool
dup
invokespecial #m    // m referencing Method java.lang.Object.<init>()V

此外,intArray[expression]++ 被编译为

… (code pushing the results of intArray and expression)
dup2
iaload
iconst_1
iadd
iastore

而且,有点花哨

public static long example3(Long[] array, int i, long l) {
    return array[i]=l;
}

编译为

   0: aload_0
   1: iload_1
   2: lload_2
   3: invokestatic  #3  // Method java/lang/Long.valueOf:(J)Ljava/lang/Long;
   6: dup_x2
   7: aastore
   8: invokevirtual #4  // Method java/lang/Long.longValue:()J
  11: lreturn

将数组类型更改为 long[] 会生成 dup2_x2

的示例

this Q&A 中所述,javac 从未使用 swapnop(在当前实现中)。但是不能仅仅因为 javac 没有使用特定指令,就不能假设没有编译器使用它。

例如还有其他 Java 编译器,例如 ECJ,但可以有 class 由其他编程语言创建的文件,或者已经是检测工具的结果,当您想要检测时,这些文件就变得相关了运行时的代码。并且javac以后的版本也可以使用之前没有使用的指令,就像之前Java 8,Java代码没有使用invokedynamic.

This discussion 指向一个场景,其中 swap 是合适的。使用 try-with-resource 时,会生成代码,在已经捕获异常的情况下处理捕获的异常。当前 javac 将其(基本上)编译为

astore        n
aload         o
aload         n
invokevirtual #x // Method java/lang/Throwable.addSuppressed:(Ljava/lang/Throwable;)V

其中 o 是保存已捕获异常的旧变量,n 将是一个全新的变量,将其编译为

时不需要它
aload         o
swap
invokevirtual #x // Method java/lang/Throwable.addSuppressed:(Ljava/lang/Throwable;)V

相反。所以这些指令并不是不需要的奇异结构。当特定代码生成器不使用它们时,这只是一个实现细节。

说到 Instrumentation,请记住 ClassFileTransformer 不能保证接收到与编译器生成的字节码完全相同的字节码,这一点也很重要。它可能是等效的字节码。

所以最重要的是,如果你想实现一个 ClassFileTransformer,你应该准备好处理每个合法的字节码。

dup 的另一个常见用例是数组初始化。

考虑代码 int[] a = new int[] {1, 2, 3}

iastore指令将整数存储到数组中。它需要堆栈上的三个值:对数组的引用、该数组中的索引和要存储的值,最重要的是,它会在调用后弹出所有三个值:

The arrayref, index, and value are popped from the operand stack. Java Virtual Machine Specification

将上述示例转换为字节码的一种天真的方法如下所示:

iconst_3
newarray       int
iconst_0               # array index
iconst_1               # value
iastore                # a[0] = 1
aload_1                # load array ref again
iconst_1
iconst_2
iastore                # a[1] = 2
aload_1                # load array ref yet again
iconst_2
iconst_3
iastore                # a[2] = 3
...
astore_1

请注意,每个 iastore 指令都会重新加载对数组的引用。

使用 dup 可以避免这种情况。在将索引和值压入堆栈之前,我们复制堆栈上的数组引用,将底部条目留在那里以供下一个 iastore 指令重新使用:

iconst_3
newarray       int
dup
iconst_0
iconst_1
iastore                # a[0] = 1
dup
iconst_1
iconst_2
iastore                # a[1] = 2
dup
iconst_2
iconst_3
iastore                # a[2] = 3
...
astore_1