Java中的运行时内存分配与编译时内存分配

4
我很困惑Java中的内存分配是在运行时还是编译时发生的。
例如:
class Test{
  int a;
  public Test(){
    a=10;
  }
};

// somewhere else
Test t = new Test();

a是在运行时还是编译时分配的?如果是在编译时,因为Java运行于直接执行已编译的.class文件的虚拟机上,这怎么可能呢?

另外:

  • a何时被赋值为10

  • 对于引用变量t,它是如何工作的?

谢谢。


这个例子无法编译(赋值应该与声明合并)。 - Sergey Kalinichenko
1
你可能会对编译时可以确定的内存分配大小与直到运行时才能确定的内存分配感到困惑。 - Jesus Ramos
引用的大小在程序运行之前是未知的。64位JVM可以使用32位或64位引用。 - Peter Lawrey
是的,也许我可以知道这两者之间的区别。任何示例都将非常有帮助。在编译时确定大小如何帮助,因为JVM分配内存并且文件在其他地方编译。 - user1649415
4个回答

9

编译时不会发生内存分配,只有在加载和运行时才会。

编译时只生成.class文件。

请记住您需要一个主类来运行程序。当您使用Java并且将类路径设置为.class文件运行程序时,会出现加载和链接等步骤。

类加载器将文件加载到permgen(永久代)中。

当调用主方法时,会创建堆栈并将本地变量放置在其中。

当运行时遇到new关键字时,它会在堆上创建对象,并分配所需的内存,例如Test所需的内存。


a 是一个原始类型,而不是对象或引用。 - Peter Lawrey
问题有点令人困惑。;) - Peter Lawrey
@PeterLawrey:我认为 OP 真正想知道的是如何将各个部分编织在一起,从加载到运行时等等,例如问题是,只是了解内存 JVM 如何处理指令(即使不完美)。在学习 Java 时,我也有类似的困惑,随着几个月的进展,逐渐明确了。 - kosa

5

本地变量和方法参数(如原始类型或引用)在编译时被指定为在堆栈上分配位置。

在运行时,这并不能保证反映它在内存中的布局。

只有在运行时才会在堆上分配对象。

Java 运行在直接处理编译 .class 文件的虚拟机上,那么如何可能呢?

只有虚拟机知道代码将如何编译,因此不可能在编译时进行内存分配。

当一个值被分配为 10 时?

在赋值发生的那一行。如果它没有使用,JIT 可以丢弃它,所以可能根本不会发生。

对于引用变量 t,同样的问题也存在。

t 在对象构造后的 = 之后被分配。


@peter-lawry 你能否请澄清一下你所说的:“本地变量和方法参数,如原始类型或引用,在编译时被概念化地分配到堆栈上。” 1. 你所说的“概念化”是什么意思?2. 如果 JVM 实际上没有运行任何东西,为什么它会在编译时有一个堆栈?那句话似乎与这句话相矛盾:“只有虚拟机知道代码将如何编译,因此不可能在编译时进行内存分配。”...我对 Java 还比较新,谢谢! - Sam Malayek
@LXXIII,“概念上”是指字节码是为一个并不存在的虚拟机而设计的。因此,虽然它为VM寄存器分配空间,但它们是否真正存在以及如何存在是在运行时确定的。 - Peter Lawrey
@LXXIII 显然的矛盾在于 javac 编译器可以分配/赋值常量、指令、寄存器、堆栈使用,但在运行时 JIT 可以重新优化它以适应实际的硬件,无论如何都可以。也就是说,在运行时机器码可能看起来完全不同。 - Peter Lawrey
当你说编译时,是指生成字节码还是将字节码转换为CPU可运行的内容? - Anthony

1
在Java中,除非它的对象被创建,否则类不会被加载,这些对象在运行时创建,因此任何成员变量(例如您代码中的“a”)都将在类被加载时分配空间,对象也是如此,在运行时分配空间。

1

这个问题有点棘手,我不确定你是否能从整个讨论中得到你想要的确切答案,因为实际上,你所问的是编译器内部的主要话题,而大多数人并不关心。

在大多数情况下,Java编译器使用自动内存管理,因此真正做出决定的是编译器本身,这也可能会随着版本的变化而改变。

但在我开始解释之前,我想澄清一下我的符号表示:

  1. 当我提到一个不是原始类型的Java对象时,我会使用[Object]。
  2. 当我提到一个具有值和标识符的内存位置时,我会使用[object]。
  3. 我会使用[primitive]来指代由Java中的原始类型之一(int、double、float等,不包括字符串)组成的[object]。
  4. 在Java中,String是一个特殊情况,虽然它是一个对象,但它可能与其他对象不同。

    [object]有一个特殊属性。它有一个值和一个标识符,标识符解析为值的过程以及发生的时间取决于绑定类型。

    有静态绑定,可以在编译时解析绑定,并且在编译时已知其值或方法。这也称为“早期”绑定。例如:

    int a = 0; // AND //像print()这样的直接函数调用;

    还有动态绑定,其中标识符和值之间的绑定或子程序到程序的绑定直到运行时才能发生。这也称为“晚期”绑定。例如:

    public void foo(java.util.List list) { list.add("bar"); }

    还有一种混合类型的绑定,但我不会谈论它,因为我没有发现Java有它。

    现在,绑定也与作用域密切相关,即变量“存在”于某个特定的作用域中。这是一个我真的不想深入探讨的话题(作用域有点棘手),这会使这篇文章成为一部小说而不是一部中篇小说。

    Java中内存分配的方式取决于几个因素:

    1. 如果在编译时已知对[Object]、[object]或[primitive]的引用,并且可以使用静态绑定,则编译器可能会在编译时为这些对象(注意我没有使用括号)分配内存。

    2. 如果在编译时无法知道对[Object]、[object]或[primitive]的引用,并且必须使用动态绑定,则编译器可能会在运行时为这些对象分配内存。

    Java处理在运行时分配的对象的方式取决于使用了哪种类型的绑定。

    1. 静态绑定
      • 作为[Object]类型的一种对象的引用将在编译时放置在堆栈上,但它们的内存在运行时分配。(懒惰)。
      • 作为[primitive]类型的一种对象将在运行时绑定和分配。
      • 字符串是一个特殊情况,但通常像[Object]的情况一样处理。
    2. 晚期绑定
      • 在运行时进行堆栈和堆上的分配。

    总之,不要担心它。这对你来说是一个大头痛。

    如果我对任何事情都错了,请有人告诉我。我有点生疏。


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