JVM在堆栈上分配变量的规则是什么?

li9yvcax  于 2022-11-07  发布在  其他
关注(0)|答案(1)|浏览(141)

64位计算机上的Long对象可能需要24个字节:12个字节用于对象头,8个字节用于长值本身,另外4个字节用于填充。我很容易地在谷歌上搜索到了这个,但是我在JVM Specification中找不到。
我在任何地方都找不到的是,原始变量类型放在堆栈上时的大小是多少。它们到底有没有被填充?* 规则 * 是什么?例如,下面方法中的变量的堆栈内存占用量是多少:

class MemoryTest{
  static void foo() {
    int anInt=0;
    long aLong=0L;
    byte aByte=0;
    short aShort=0;
    // some code that uses the vars above
  }
}
mzaanser

mzaanser1#

我很容易在谷歌上搜索到这个,但是我在JVM规范中找不到它。
这是因为它是一个实现细节。规范没有说明的一切都是实现自己做自己事情的空间。在这个例子中,规范在Python 2.7中特别指出:
Java虚拟机不要求对象具有任何特定的内部结构。

JVM数据类型

JVM的数据类型在2.3中定义。
整数类型有:

  • byte,其值为8位有符号二进制补码整数,默认值为零
  • short,其值为16位有符号二进制补码整数,默认值为零
  • int,其值为32位有符号二进制补码整数,默认值为零
  • long,其值为64位有符号二进制补码整数,默认值为零
  • char,其值为16比特不带负数号的整数,代表Basic Multilingual Plane中的Unicode字码指标,以UTF-16编码,预设值为null字码指标('\u0000')

浮点类型包括:

  • float,其值与32位IEEE 754 binary 32格式中可表示的值完全对应,并且其默认值为正零
  • double,其值与64位IEEE 754 binary 64格式的值完全对应,其默认值为正零

它们只是具有您所期望的大小,没有像 Package 类那样的头。然而,JVM指令集是有限的,并不是每种类型都有对应的指令。实际上,大多数指令都是针对intlongfloatdouble。(更多信息请参见下表)因此,最终您几乎总是要使用这些类型。

操作数堆栈和局部变量

注意,基本类型的值并不是“直接“放在堆栈上。JVM堆栈存储”帧“,这些帧“用于存储数据和部分结果,以及执行动态链接、返回方法的值和分派异常”。
在帧上,有一个“操作数堆栈”,其中存储部分结果。例如,一个实现可能会先将代码中的0压入操作数堆栈,然后再将它们弹出到局部变量中。
该规范对操作数堆栈上的内容的大小进行了如下说明:
操作数堆栈上的每个条目都可以保存任何Java虚拟机类型的值,包括longdouble类型的值。
[...]
在任何时间点,算子堆栈都有相关的深度,其中longdouble型别的值会贡献两个单位的深度,而任何其他型别的值则贡献一个单位的深度。
根据规范,局部变量也存储在帧中的一个数组中。与您所请求的内容相关的部分是:
单个局部变量可以保存booleanbytecharshortintfloatreferencereturnAddress类型的值。一对局部变量可以保存longdouble类型的值。
[...]
类型long或类型double的值占用两个连续的局部变量。这样的值只能使用较小的索引来寻址。例如,存储在局部变量数组中索引n处的类型double的值实际上占用索引为n和n+1的局部变量;但是,位于索引n+1处的局部变量不能从加载。它可以存储到中。但是,这样做会使局部变量n的内容无效。
Java虚拟机不要求n为偶数。直观地说,longdouble类型的值在局部变量数组中不需要64位对齐。实现者可以自由决定使用为该值保留的两个局部变量来表示这些值的适当方式。

您的示例

对于您的代码,我们需要做一些合理的假设。让我们假设生成了下面的类文件(从javap看)。我使用javac -g MemoryTest.java编译了这个文件。

Classfile /Users/mulangsu/Desktop/MemoryTest.class
  Last modified 2022/09/24; size 264 bytes
  SHA-256 checksum c1aa63404c5e590ef52116de586a91d75deb8727ab749cbd1e0d6fb4197357c2
  Compiled from "MemoryTest.java"
class MemoryTest
  minor version: 0
  major version: 61
  flags: (0x0020) ACC_SUPER
  this_class: #7                          // MemoryTest
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Class              #8             // MemoryTest
   #8 = Utf8               MemoryTest
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               foo
  #12 = Utf8               SourceFile
  #13 = Utf8               MemoryTest.java
{
  MemoryTest();
    descriptor: ()V
    flags: (0x0000)
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  static void foo();
    descriptor: ()V
    flags: (0x0008) ACC_STATIC
    Code:
      stack=2, locals=5, args_size=0
         0: iconst_0
         1: istore_0
         2: lconst_0
         3: lstore_1
         4: iconst_0
         5: istore_3
         6: iconst_0
         7: istore        4
         9: return
      LineNumberTable:
        line 3: 0
        line 4: 2
        line 5: 4
        line 6: 6
        line 8: 9
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            2       8     0 anInt   I
            4       6     1 aLong   J
            6       4     3 aByte   B
            9       1     4 aShort   S
}
SourceFile: "MemoryTest.java"

有趣的是,使用的所有指令都是int和long指令。没有bipushsipush,尽管编译器本可以使用它,但我猜这会使类文件略大。
这就是我之前描述的“将0放在操作数堆栈上,然后将其弹出到局部变量”的行为。
如果我们还假设每个局部变量槽被实现为4个字节,则局部变量将总共占用20个字节。anIntaByteaShort每个占用一个槽,并且aLong占用2个槽,因此总共占用5个槽。

相关问题