Pushing variables to Stack 和 Variables living in the Stack 的区别?

Pushing variables to Stack and Variables living in the Stack difference?

所以我知道存在2个内存区域:StackHeap

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

现在我会试着把我的困惑传递给你:

例如这个简单的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 推入的堆栈还是这些单独的堆栈?

现在,如果我在第一个压入堆栈的对象上调用一个方法,由于堆栈指针指向我压入的最后一个元素,它将如何处理?

你应该看看 structure of Java stack frame

一个java 栈帧包含3个东西:

  1. A local variable table
  2. An operand stack
  3. A reference to class's constant pool AKA Frame Data

因此,push a null reference onto the stack --> 将引用推送到 操作数堆栈

store a reference into local variable 1 --> 将引用存储到 局部变量的槽 1 table

JVM 中每个线程存在一个堆栈。每个堆栈由几个框架组成:每个方法调用都会创建一个新框架,当方法调用完成时,框架会被销毁。

在堆栈框架内有两个区域:

  1. 操作数栈(不要把这里的"stack"这个词和JVM栈本身搞混了——这里的栈表示这个区域是后进先出的-out结构)。
  2. 局部变量数组,其中每个变量都有一个索引(从零开始)。

根据 JVM 实现,它们在内存中可能连续也可能不连续。从逻辑上讲,它们是堆栈框架的两个独立部分。

description of aconst_null 中所述,aconst_null 指令将 null 对象引用压入 操作数堆栈

并且如 description of astore_<n> 中所述(其中 n 可以是 0、1、2 或 3):

The <n> must be an index into the local variable array of the current frame (§2.6). The objectref on the top of the operand stack must be of type returnAddress or of type reference. It is popped from the operand stack, and the value of the local variable at <n> is set to objectref.

因此在您的示例中,语句 Object foo = null 转换为以下内容:

  1. null(指向"nothing"的特殊引用)压入操作数栈的顶部。
  operand stack
   __________
  |   null   | <-- null is pushed on the operand stack
  |__________|
  |          |
  |__________|
  |          |
  |__________|
  1. 从操作数栈弹出引用,存入索引为1的局部变量。该局部变量对应foo.
  operand stack                           local variables
   __________      _______________ _______________ _______________ _______________
  |          |    |      args     |   foo (null)  |               |               |
  |__________|    |_______0_______|_______1_______|_______2_______|_______3_______|
  |          |                    store null in LV#1 
  |__________|
  |          |
  |__________|

Object bar = null 执行相同的步骤,只是 null 存储在索引 2 处的局部变量中。

来源:Java 虚拟机规范(参见 this section)。

你可以把操作数栈想象成临时变量。它对每个方法调用都是局部的,其大小可以在编译时确定。

如果你想对任何种变量(局部变量、静态变量或非静态变量)做任何事情,你可以通过操作数栈来完成。 Java字节码指令主要与操作数堆栈一起工作。

例如,

  • foo = bar对应aload_2astore_1,简单来说就是将局部变量2的值压入操作数栈并且将操作数堆栈顶部的任何内容弹出到局部变量 1
  • if (foo == null) ... 将对应于 aload_1ifnonnull 5,其中后者告诉 JVM:如果操作数堆栈顶部的任何内容不为空,则跳转到接下来的 5 个指令偏移量;否则,继续下一条指令.
  • int x = args.length对应aload_0arraylengthistore_3,即压入局部变量0将数组弹出到操作数堆栈顶部并将其长度推回弹出整数并将其存储在局部变量 3
  • iaddisubimulidiv等数值运算从操作数栈中弹出两个整数值并将结果压回
  • 调用方法时,弹出操作数栈并将其作为参数传递给新方法的局部变量。
  • putstatic/getstatic pops/pushes to/from 静态变量
  • putfield/getfield pops/pushes to/from 非静态变量

这是同一个堆栈。

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

在一个简单的jvm中

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

将堆栈中的某些内容分配给局部变量就是从堆栈顶部复制到附近的某个地址,在同一内存区域中之前的几个位置。

在您的示例 astore 1 期间:

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