Java堆空间与栈内存 - Java中的内存分配

Java内存中Java堆空间,Java栈内存,Java内存分配以及它们之间有什么区别。经常会接触但很也很容易模糊之间的界限。

Java内存分配

JVM-Architecture

方法区:

方法区域在虚拟机启动时创建,虽然它在逻辑上是堆的一部分,但它可以被垃圾收集,也可能不可以,而已知的对堆的收集不是可选的而是强制的。方法区域可以是固定大小的,也可以是根据需要进行扩展,方法区域的内存不需要是连续的。

方法区域存储类的结构,例如运行时常量池,属性和方法数据,构造函数和方法的代码,包括类,实例和接口初始化中使用的特殊方法。

方法区是所有线程共享的,PermGen(永久代)是方法区的具体实现,不是所有JVM都有PermGen(永久代),但都会有方法区的概念。空间不足时会抛出java.lang.OutOfMemoryError: PermGen space。

JDK1.8 HotSpot的实现取消了PermGen space,被Metaspace(元空间)取而代之。所以java.lang.OutOfMemoryError: PermGen space也就不存在了,而Metaspace被放到了直接内存中,直接使用本地内存,Metaspace摆脱了堆空间的束缚,仅受本地内存大小的限制。

堆空间:

堆内存为所有线程共享,空间不足时会抛出OutOfMemoryError。堆区域表示运行时数据区域,为所有类实例和阵列分配内存,并在虚拟机启动期间创建。

对象的堆存储由自动存储管理系统回收,堆的大小可以是固定的,也可以是动态的(基于系统的配置),并且为堆分配的内存不必是连续的。Java虚拟机实现允许对堆的初始大小进行控制,可以动态扩展或收缩,以控制堆的最大和最小。

栈内存:

栈空间的大小可以调整,空间不足时会抛出StackOverflowError。每个JVM线程都有一个与线程同时创建的私有栈空间,栈存储所有的帧,帧用于存储数据和部分结果,执行动态链接,并返回方法的值以及调度异常。

它保存局部变量和部分结果,并在方法调用和返回中起作用。因为栈永远不会被直接操作,除了push和pop帧之外,帧可以是从堆中分配。与堆类似,栈的内存不需要是连续的。

规范允许栈可以是固定或动态的,如果它是固定大小,则可以在创建栈时,独立地选择每个栈的大小。

PC寄存器:

也叫程序计数器,每个线程有自己的程序计数器。

当jvm执行方法时,由于Java应用程序可以包含一些本地代码(例如,使用本地库),因此会有本地和非本地方法之分,如果该方法不是本地方法(即Java代码),则PC寄存器包含当前正在执行的JVM指令的地址。如果该方法是本地方法,则JVM的PC寄存器的值是undefined(未定义的)。

Java虚拟机的PC寄存器足够宽,可以在特定平台上,保存返回地址或本机指针。现在这一切都与JVM内部的内存区域结构有关。

本地方法栈:

本机方法栈称为C栈,它用于支持本机方法(Native方法,用非Java编程语言编写的方法),通常在创建每个线程时为每个线程分配,JVM无法加载本机方法且它不依赖于传统栈,Java虚拟机实现无需提供本机方法栈。本机方法栈的大小可以是固定的也可以是动态的。

Java堆空间

Java运行时空间,由Java运行时用于为Objects和JRE classes分配内存。每当创建对象时,它总是在堆空间中创建。

垃圾收集在堆内存上运行,以释放没有任何引用的对象所占用的内存。在堆空间中创建的任何对象都具有全局访问权限,可以从应用程序的任何位置引用到。

Java栈内存

Java栈内存用于执行线程。它们包含方法中特定类型的值,及该方法对堆中对象的引用,特定值生命很短暂。

堆栈存储器始终以LIFO(后进先出)顺序引用。每当调用一个方法时,都会在栈内存中创建一个新块,以便保存该方法中的本地原始值和该方法中的对某些对象的引用。

方法调用结束后,块将变为未使用状态,可供于下一个方法使用。与堆内存相比,栈内存所占空间非常少。

Java编程中的堆空间与栈内存

通过一个简单的程序来理解堆和栈的内存使用情况。

package com.xieyonghui.mt;

public class Memory {

    public static void main(String[] args) { // Line 1
        int i=1; // Line 2
        Object obj = new Object(); // Line 3
        Memory mem = new Memory(); // Line 4
        mem.foo(obj); // Line 5
    } // Line 9

    private void foo(Object param) { // Line 6
        String str = param.toString(); //// Line 7
        System.out.println(str);
    } // Line 8

}

下图显示了栈空间和堆内存,参考上述程序,理解它们如何用于存储原始类型、对象和引用。来完成程序的执行步骤。

Java-Heap-Stack-Memory

一旦运行程序,java类加载器就会将所有运行时类加载到堆空间中。当在第1行找到main()方法时,Java Runtime会创建由main()方法线程需要的栈内存。

在第2行创建原始局部变量,因此它被创建并存储在main()方法的栈内存中。

由于在第3行创建了一个Object,它在堆内存中创建,栈内存包含了对它的引用,当我们在第4行创建Memory对象时,会发生类似的过程。

现在,当调用第5行foo()方法时,会在栈的顶部创建块以供foo()方法使用。由于Java是按值传递的(传引用),因此在第6行的foo()栈块中创建了对Object的新引用。

在第7行创建一个字符串,它在堆空间的String Pool中,并在foo()栈空间中为它创建一个引用。
foo()方法在第8行终止,此时在栈中为foo()分配的内存块变为空闲。

在第9行中,main()方法终止,并把为main()方法所创建的栈内存进行销毁。此程序也在此行结束,因此Java Runtime释放所有内存并结束程序的执行。

Java堆空间和栈内存之间的区别

基于以上解释,可以很容易地得出Heap和Stack内存之间的以下差异。堆内存由应用程序的所有部分使用,而栈内存仅由一个执行线程使用。

每当创建一个对象时,它总是存储在堆空间中,而栈存储器包含对它的引用。栈内存仅包含本地原始类型的变量和堆空间中对象的引用。

存储在堆中的对象是全局可访问的,而栈内存不能被其他线程访问。栈中的内存管理以LIFO方式完成,而在堆内存中则更复杂,因为它是全局使用的。堆内存分为Young-Generation,Old-Generation等。

栈内存是短暂的,而堆内存从应用程序执行的开始到结束都存在。可以使用-Xms和-Xmx JVM选项,来定义堆内存的启动大小和最大限制。可以使用-Xss来定义栈内存大小。

当栈内存已满时,Java运行时抛出java.lang.StackOverFlowError,而如果堆内存已满,则抛出java.lang.OutOfMemoryError: Java Heap Space错误。

与堆内存相比,栈内存空间非常小,由于栈内存分配(LIFO)的简单性,与堆内存相比,栈内存非常快。