JVM中操作数栈的作用是什么?

jia*_*g24 3 java jvm bytecode jvm-bytecode

JVM 运行时数据区为正在执行的每个方法单独的堆栈。它包含操作数堆栈和局部变量。每次加载变量时,都需要const先到操作数栈,然后store再到局部变量。为什么不直接操作局部变量表,还有一些看似重复的工作呢?

Hol*_*ger 5

具有直接操作数的指令集必须对每条指令中的操作数进行编码。相反,对于使用操作数堆栈的指令集,操作数是隐式的。

在查看诸如将常量加载到变量之类的小操作时,隐式参数的优势并不明显。这个例子是将“操作码、常量、操作码、变量索引”序列与“操作码、常量、变量索引”进行比较,所以看起来直接寻址更简单、更紧凑。

但是让我们看看,例如 return Math.sqrt(a * a + b * b);

假设变量索引从零开始,字节码看起来像

   0: dload_0
   1: dload_0
   2: dmul
   3: dload_2
   4: dload_2
   5: dmul
   6: dadd
   7: invokestatic  #2                  // Method java/lang/Math.sqrt:(D)D
  10: dreturn
  11 bytes total
Run Code Online (Sandbox Code Playgroud)

对于直接寻址架构,我们需要类似的东西

dmul a,a ? tmp1
dmul b,b ? tmp2
dadd tmp1,tmp2 ? tmp1
invokestatic #2 tmp1 ? tmp1
dreturn tmp1
Run Code Online (Sandbox Code Playgroud)

我们必须用索引替换名称。

虽然这个序列由较少的指令组成,但每条指令都必须对其操作数进行编码。当我们希望能够寻址256个局部变量时,每个操作数需要一个字节,所以每条算术指令需要三个字节加操作码,调用需要两个加操作码和方法地址,返回需要一个加操作码。所以对于字节边界的指令,这个序列需要 19 个字节,明显多于等效的 Java 字节码,同时被限制为 256 个局部变量,而字节码最多支持 65536 个局部变量。

这展示了操作数堆栈概念的另一个优势。Java字节码允许不同,优化的指令,例如用于装载一个整常数结合有iconst_nbipushsipush,和ldc用于存储它到一个变量有istore_nistore n,和wide istore n。当应该支持广泛的常量和变量数量但仍支持紧凑指令时,具有直接变量寻址的指令集将需要针对每个组合的不同指令。同样,它需要所有算术指令的多个版本。

您可以使用双操作数形式而不是三操作数形式,其中源变量之一也指示目标变量。这会产生更紧凑的指令,但如果之后仍需要操作数的值,则需要额外的传输指令。操作数堆栈形式仍然更加紧凑。

请记住,这仅描述了操作。执行环境在执行代码时不需要严格遵循这个逻辑。所以除了最简单的解释器之外,所有的JVM实现在执行之前都会把它转换成不同的形式,所以原来存储的形式对实际执行性能没有影响。它只影响空间要求和加载时间,这两者都受益于更紧凑的表示。这尤其适用于通过可能较慢的网络连接传输的代码,这是 Java 最初设计的用例之一。