JVM dup指令的使用案例

7

Java字节码指令集提供了各种形式的dup指令。我不太理解这些指令和swap指令的用途。当编译时,哪些Java代码会产生带有这些指令的字节码?


2
我认为这个问题并不太宽泛。有一些简单的实际例子,每天的Java代码都包含在内,例如对象实例化通常使用dup。此外,intArray [expression] ++编译为…,dup2,iaload,iconst_1,iadd,iastore。如此问答中所讨论的,javac从未使用swapnop(在当前实现中)。 - Holger
@Holger dupx 变体,例如 dup_x1dup2_x1 等怎么处理?我正在为 Java 语言编写一个工具,如果我能忽略这些指令,那会让我的生活变得轻松得多。 - saga
1
仅仅因为javac没有使用特定的指令,你不能假设没有编译器使用它。例如,还有其他Java编译器,比如ECJ,但也可能有其他编程语言创建的类文件或已经是一个工具的结果。而且事情是会变化的。在Java 8之前,Java代码没有使用invokedynamic - Holger
3个回答

3
我不知道javac什么时候使用它,但是在我们生成代码时经常使用DUP和SWAP。例如,如果您要执行以下操作的等效操作:
x.setCharm(y);
x.setSpin(z);

如果你想要加载x并立即重复使用它,那么需要在第一次调用方法之前将其DUP,因为第一次调用会将其从堆栈中取出,而你想要使用两次。

当你需要执行类似于以下操作时,SWAP很有用:

y = x.getCharm();
z.setCharm(y);

首先,第一条指令会将 y 放在栈顶,然后你需要把 z 入栈,并执行 SWAP 操作,这样你就能够正确地调用第二条指令。


3

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的示例。

此Q&A中所讨论的那样,javac从不使用swapnop(在当前实现中)。但是仅因为javac不使用特定指令,您不能假设没有编译器使用它。

例如,还有其他Java编译器,如ECJ,但可能会创建由其他编程语言创建的类文件或已经是工具的结果,这在您想要在运行时检测代码时变得重要。而且,未来版本的javac也可以使用以前没有使用过的指令,就像在Java 8之前,Java代码没有使用invokedynamic一样。

此讨论指出了一种情况,在该情况下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

所以说,这些指令并不是从未需要的外来构造。当特定代码生成器不使用它们时,这只是一种实现细节。

谈到仪器化,还要记住 ClassFileTransformer 不保证接收与编译器生成的完全相同的字节码。它可能是等效的字节码。

因此,总之,如果你想要实现一个 ClassFileTransformer,你应该准备处理每个合法的字节码。


0

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

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

iastore 指令将整数存储到数组中。它需要栈上的三个值:对数组的引用、该数组中的索引和要存储的值,并且在调用后弹出这三个值:

数组引用、索引和值从操作数栈中弹出。 Java虚拟机规范

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

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

我认为dup指令比从本地变量加载要稍微“便宜”一些。我尝试使用两个版本(aload vs. dup)对此进行基准测试(使用1,000个值的int数组初始化1,000,000次),但我的结果并不确定。差异可能更多地体现在最佳实践方面(避免重新加载已经可用的内容),而不是真正的性能影响。虽然我可能是错的。 - martin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接