将变量推入堆栈和变量生存在堆栈中的区别?

13

我知道有两个内存区域:

我也知道如果你创建一个局部变量,它会存在于栈中,而不是堆中。栈会随着我们向其中推入数据而增长,就像这样:

enter image description here

现在我将尝试把我感到困惑的事情传达给你:

例如,这段简单的Java代码:

public class TestClass {
    public static void main(String[] args)  {
        Object foo = null;
        Object bar = null;
    }
}

被翻译成这个字节码:

public static void main(java.lang.String[]);
  Code:
   Stack=1, Locals=3, Args_size=1
   0:   aconst_null
   1:   astore_1
   2:   aconst_null
   3:   astore_2
   4:   return

LineNumberTable: 
line 5: 0
line 6: 2
line 7: 4

LocalVariableTable: 
Start  Length  Slot  Name   Signature
0      5      0    args       [Ljava/lang/String;
2      3      1    foo       Ljava/lang/Object;
4      1      2    bar       Ljava/lang/Object;

根据定义,acons_null 是:

push a null reference onto the stack

astore_1 是:

store a reference into local variable 1
我有点疑惑:我们将foo推入栈中,然后又将其存储在栈中?将引用存储在本地变量中是什么意思?这个本地变量存在于哪里?是与我们推foo的堆栈相同还是分开的?现在,在那个点上,如果我在堆栈中调用第一个对象的方法,由于堆栈指针指向我推入的最后一个元素,它会如何被处理?
4个回答

13
在JVM中,每个线程都有一个堆栈。每个堆栈由多个帧构成:每次方法调用都会创建一个新的帧,当方法调用结束时,该帧就会被销毁。
在堆栈帧内部有两个区域:
1. 操作数栈(不要将此处的“栈”与JVM堆栈本身混淆——这里的“栈”表示该区域是一个后进先出的结构)。 2. 一个本地变量数组,其中每个变量都有一个索引(从零开始)。
根据JVM实现的不同,它们可能或可能不是连续的内存块。从逻辑上讲,它们是堆栈帧的两个独立部分。
aconst_null指令的描述所述,aconst_null指令会将null对象引用推送到操作数栈上方。
而正如astore_<n>指令的描述所述(其中n可以是0、1、2或3):
“n必须是当前帧的局部变量数组(§2.6)中的索引。堆栈顶部的objectref必须是returnAddress类型或reference类型。它从操作数栈中弹出,并将局部变量在n处的值设置为objectref。”
因此,在您的示例中,语句Object foo = null转换为以下内容:
1. 将null(一个指向“无”的特殊引用)推送到操作数栈的顶部。
  operand stack
   __________
  |   null   | <-- null is pushed on the operand stack
  |__________|
  |          |
  |__________|
  |          |
  |__________|
从操作数栈中弹出引用并将其存储在索引为1的本地变量中。该本地变量对应于foo
  operand stack                           local variables
   __________      _______________ _______________ _______________ _______________
  |          |    |      args     |   foo (null)  |               |               |
  |__________|    |_______0_______|_______1_______|_______2_______|_______3_______|
  |          |                    store null in LV#1 
  |__________|
  |          |
  |__________|
对于Object bar = null,同样的步骤被执行,只是将null存储在索引2的本地变量中。
来源:Java虚拟机规范(参见此节)。

1
非常好,只有一件事要补充。虽然操作数栈在概念上可以增长,但堆栈帧的大小是恒定的,因为每个方法声明了其最大操作数栈大小。这就是将堆栈组织成帧的原因,内存只在方法入口处分配,而不是每个推送值的指令都需要分配内存。 - Holger

2
你应该查看Java堆栈帧结构
一个Java堆栈帧包含三个部分:
  1. 本地变量表
  2. 操作数栈
  3. 对类常量池的引用,也称为框架数据
因此,将null引用推送到栈中--->将引用推送到操作数栈中。
将引用存储到本地变量1中--->将引用存储到本地变量表的1号槽中。

2
您可以将操作数栈视为临时变量。它仅在每个方法调用中有效,并且其大小可以在编译时确定。
如果您想对任何类型的变量(局部变量、静态变量或非静态变量)执行任何操作,都可以通过操作数栈来完成。Java字节码指令主要使用操作数栈。
例如:
- `foo = bar` 对应于 `aload_2` 和 `astore_1`,它们的意思是“将局部变量2的值推送到操作数栈上”和“弹出操作数栈顶部的内容并将其存储到局部变量1中”。 - `if (foo == null) ...` 对应于 `aload_1` 和 `ifnonnull 5`,其中后者告诉JVM:“如果操作数栈顶部的内容不为空,则跳转到下一个偏移量为5的指令;否则,继续执行下一条指令。” - `int x = args.length` 对应于 `aload_0`、`arraylength`、`istore_3`,它们的意思是“将局部变量0推送到操作数栈上”、“弹出操作数栈顶部的数组并将其长度推回”、“弹出整数并将其存储在局部变量3中”。 - 数值运算如 `iadd`、`isub`、`imul`、`idiv` 从操作数栈中弹出两个整数值并将结果推回。 - 在调用方法时,操作数栈会被弹出并作为参数传递给新方法的局部变量。 - `putstatic`/`getstatic` 弹出/推送静态变量。 - `putfield`/`getfield` 弹出/推送非静态变量。

你能详细说明一下局部变量吗? - Koray Tugay
本地变量包括方法参数和您在方法中声明的变量。它们以数字为索引,从0开始计数。如果是非静态方法,则变量0为this。它们如同平常一样有类型(int、long、float、_reference_等)。JVM有适用于每种类型的指令,例如 iload、lload、fload、_aload_等。 - dejvuth

1

这是同一个栈。

或者至少你可以认为它是同一个栈,这实际上取决于jvm的实现。

在简单的jvm中

当调用方法时,它会在栈上为局部变量保留空间。它基本上通过增加堆栈指针来打开其局部变量的空间。方法的父对象(如果是实例方法)和方法的参数是第一个局部变量。

将栈上的内容分配给局部变量是将其从堆栈顶部复制到相邻地址的过程,在同一内存区域中的几个位置之前。

在你的示例中的 astore 1 过程中:

locals/stack
[local 0] // args
[local 1] // foo   <--+
[local 2] // bar      |
..return address..    |
[stack 0] // null  ---+

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