Java堆栈内存管理

28

我想知道以下程序中内存是如何分配的:

public class MemoryClass {

    public static void main(final String[] args) {
        int i = 0;
        MemoryClass memoryClass = new MemoryClass();
        memoryClass.myMethod(memoryClass);
    }

    private void myMethod(final Object obj) {
        int i = 1;
        String s = "HelloWorld!";

    }

}

据我所知,以下图表描述了内存分配的过程:


Basic runtime memory allocation

在上面的图表中,位于堆栈内存中的memoryobjs实际上是它们所引用的“实际对象”在堆内存中的位置。

以下是我想到的问题:

  1. s的方法存储在哪里?
  2. 如果我在myMethod中创建了另一个MemoryClass对象,JVM是否会再次为相同的方法在堆栈内存中分配内存?
  3. 如果JVM立即释放给myMethod分配的内存,当存在问题2所述的情况时,它将如何管理?(仅当JVM多次为同一方法分配内存时适用)
  4. 如果我只声明了s但未初始化,JVM是否仍然会向java.lang.String类的所有方法分配内存?为什么?


Java的好处在于它将所有的内存管理都“隐藏”起来,让垃圾回收器接管控制。你为什么需要去深入研究呢? - byxor
10
仅出于好奇 - Adit A. Pillai
1
据我所知,有两个直接的原因:1. 当发生SOF或OOM错误时,您必须了解它们以修复错误;2. 在性能调优中尝试更好地管理内存时,您必须理解它们。 - Hearen
3个回答

30
方法s存储在哪里? 它们存储在String类对象中,当程序第一次引用String时,由ClassLoader对象加载该对象。在我上次阅读相关信息时,所有的JVM实现都不会在类对象被加载后释放内存。它在堆上。 如果我在myMethod内创建了另一个MemoryClass对象,JVM是否会在堆栈内再次为相同的方法分配内存? 不会,对象的方法和数据是分开存储的,这是因为JVM从未需要多个方法的副本。 如果JVM分配多次给同一方法,myMethod执行完成后,JVM会立即释放分配给它的内存吗?如果是,它将如何处理问题2中提到的情况? 不会。Java通常不会立即释放存储在堆上的内容的内存。这样会使程序运行速度变慢。仅当垃圾收集器运行时,它才会释放内存,并且仅在垃圾收集器运行算法决定是时候回收内存的时候才会释放。 如果我只声明了s而没有初始化它,JVM是否仍会为java.lang.String类的所有方法分配内存?如果是,为什么? 这取决于JVM实现和编译器。如果您声明一个变量但从未使用它,编译器很可能(并且通常)会注意到它没有用,并且不会将其放入类文件中。如果它不在类文件中,那么它就永远不会被引用,因此它及其方法也不会被加载等。如果编译器仍然将其放入类文件中,但从未引用过,则ClassLoader没有任何理由将其加载,但我有点模糊是否会加载它。这可能取决于JVM实现;它是因为类的变量还是只有在引用时才加载?多少ClassLoader算法可以跳舞在4位数的PIN码上?

我鼓励你阅读关于JVM和ClassLoaders等方面的内容;通过阅读工作原理的解释,你会比通过想出例子去探究它获得更多的知识。


为什么第二个 MemoryClass 对象的方法没有像 s 一样存储在堆上? - Adit A. Pillai
1
它们存储在堆中,但只有一个副本。大多数关于为对象分配内存的讨论并没有做出区别,但是当我们谈论为对象的实例分配内存时,人们理解系统仅为数据分配了额外的内存。没有必要为方法的相同副本分配内存。我不确定,但可能方法与该类的类对象一起存储,因为只需要一个副本。(不同的类加载器将加载不同的类对象,但这超出了我的解释范围)。 - arcy

16

首先要做的事情:我假设您阅读了this文章(因为那里我看到一个非常类似于您的图表),所以我不会引用或突出显示那篇文章中提到的任何观点,并尝试回答您在那篇文章中没有涉及到的问题。

阅读您所有的问题,我的印象是您清楚地知道内存是如何在堆栈中分配的,但对于类的元数据有疑问,即类的方法存储在内存的哪个位置以及如何回收它们。因此,让我首先尝试解释JVM内存区域:


JVM内存区域

让我从这两张描述JVM内存区域的图开始:

图表来源

enter image description here

图表来源

enter image description here

现在,如上图所示,JVM内存的树形结构清晰可见。我将尝试阐明同样的内容(@Adit:请注意您关心的区域是PermGen Space或非堆内存的永久代)。
堆内存 - 年轻代 - Eden空间 - Survivor空间 - 老年代 - Tenured Generation
非堆内存 - 永久代 - 代码缓存(我认为只有HotSpot Java VM包含)
堆内存是Java虚拟机为所有类实例和数组分配内存的运行时数据区。堆可以是固定大小或可变大小。垃圾收集器是一种自动内存管理系统,用于回收对象的堆内存。
年轻代是创建所有新对象的地方。当年轻代填满时,会执行垃圾收集。这种垃圾收集称为Minor GC。年轻代分为以下两部分。

伊甸园空间: 大多数对象最初被分配内存的池。

幸存者空间: 包含在伊甸园空间垃圾收集后幸存的对象的池。

老年代

老年代包含长期存在并在多次 Minor GC 后仍然存活的对象。通常,当老年代满时进行垃圾收集。老年代垃圾收集称为 Major GC,并且通常需要更长时间。老年代包括以下部分:

持久代: 包含在幸存者空间存活一段时间的对象的池。

非堆内存

非堆内存包括一个供所有线程共享的方法区和 Java VM 内部处理或优化所需的内存。它存储每个类的结构,例如运行时常量池、字段和方法数据以及方法和构造函数的代码。方法区在逻辑上属于堆的一部分,但是根据实现,Java VM 可能不会对其进行垃圾回收或压缩。与堆内存一样,方法区可能具有固定或可变的大小。方法区的内存不需要连续。

永久代

池包含虚拟机本身的所有反射数据,例如类和方法对象。对于使用类数据共享的Java VM,此代被分为只读区域和读写区域。

代码缓存

HotSpot Java VM还包括一个代码缓存,其中包含用于编译和存储本机代码的内存。


具体回答OP的问题

s的方法存储在哪里?

非堆内存 --> 永久代

如果我在myMethod中创建了另一个MemoryClass对象,JVM是否会在堆栈内存中为相同的方法再次分配内存?

堆栈内存仅包含局部变量,因此您的new MemoryClass的ORV(对象引用变量)仍将在myMethod的堆栈帧中创建,但是JVM不会再次加载所有MemoryClass的方法、元数据等到"永久代"。

JVM只加载一次类,当它加载类时,为该类在“永久代”上分配空间,而这仅在JVM加载类时发生一次。

如果我Method的执行完成后JVM释放分配给其的内存,如果JVM多次分配内存给相同的方法,它会如何管理所述情况?

Stack frame created for myMethod将从栈内存中删除,因此为局部变量创建的所有内存都将清除,但这并不意味着JVM将清理为myMethod创建对象分配在"永久代"中的内存。

如果我只声明s而没有初始化它,情况会怎样?JVM是否仍会为java.lang.String类的所有方法分配内存?如果是,为什么?

具体而言,对于String类,JVM会在太早的时候以“永久代”的方式分配String的空间,而无论您是否初始化字符串变量,从“永久代”的角度来看都没关系。
就其他用户定义的类而言,JVM将在定义类时加载类并在“永久代”中分配内存,即使您不创建该类的对象,也会在“永久代”(非堆区域)中分配内存,当您创建一个类的对象时,内存将在“伊甸园空间”(堆区域)中分配。

以上信息来源和进一步阅读:


@Adit:通常情况下,当已经提供的答案不够“详细”和充分时,才会放置赏金。所以我想知道为什么你在这个问题上放置了赏金,因为“arcy”在“12月13日”已经给出了答案,而你最近才放置了赏金? - hagrawal7777

2
由于arsy的回答和hagrawal的回答已经很清楚了,我只想阐述第四个问题:
如果我只声明了s而没有初始化它,那么JVM是否仍会为java.lang.String类的所有方法分配内存?如果是,为什么?
基本上,虽然包含字段和方法信息的类数据存储在永久代(从JDK-8开始是元空间)中,但需要注意的是,在java.lang.String类中的对象(例如char[],它保存该字符串的所有字符信息)才会在堆上分配数据。
这种情况只有在创建一个新的字符串对象时才发生,可以使用“new”关键字创建或通过创建新的字符串文字(例如:“helloworld”)。

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