java 中堆内存和栈内存详解

根据《Java 虚拟机规范》的规定,运行时数据区通常包括这几个部分:

jvm memory management

程序计数器 (Program Counter Register)

一块较小的内存空间,它是当前线程所执行的字节码的行号指示器,字节码解释器工作时通过改变该计数器的值来选择下一条需要执行的字节码指令,分支、跳转、循环等基础功能都要依赖它来实现。每条线程都有一个独立的的程序计数器,各线程间的计数器互不影响,因此该区域是线程私有的。

当线程在执行一个 Java 方法时,该计数器记录的是正在执行的虚拟机字节码指令的地址,当线程在执行的是 Native 方法(调用本地操作系统方法)时,该计数器的值为空。另外,该内存区域是唯一一个在 Java 虚拟机规范中么有规定任何 OOM(内存溢出:OutOfMemoryError)情况的区域。

Java 虚拟机栈 (JVM Stack)

在函数中定义的一些 基本类型 的变量和 对象的引用变量 都是在函数的栈内存中分配。当在一段代码块中定义一个变量时,java 就在栈中为这个变量分配内存空间,当超过变量的作用域后,java 会自动释放掉为该变量分配的内存空间,该内存空间可以立刻被另作他用。

该区域也是线程私有的,它的生命周期也与线程相同。虚拟机栈描述的是 Java 方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧,栈它是用于支持续虚拟机进行方法调用和方法执行的数据结构。对于执行引擎来讲,活动线程中,只有栈顶的栈帧是有效的,称为当前栈帧,这个栈帧所关联的方法称为当前方法,执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作。栈帧用于存储局部变量表、操作数栈、动态链接、方法返回地址和一些额外的附加信息。在编译程序代码时,栈帧中需要多大的局部变量表、多深的操作数栈都已经完全确定了,并且写入了方法表的 Code 属性之中。因此,一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。

本地方法栈 (Native Method Stack)

该区域与虚拟机栈所发挥的作用非常相似,只是虚拟机栈为虚拟机执行 Java 方法服务,而本地方法栈则为使用到的本地操作系统(Native)方法服务。

Java 堆 (Heap)

Java Heap 是 Java 虚拟机所管理的内存中最大的一块,它是所有线程共享的一块内存区域。几乎所有的对象实例和数组都在这类分配内存。Java Heap 是垃圾收集器管理的主要区域,因此很多时候也被称为 “GC 堆”。

根据 Java 虚拟机规范的规定,Java 堆可以处在物理上不连续的内存空间中,只要逻辑上是连续的即可。如果在堆中没有内存可分配时,并且堆也无法扩展时,将会抛出 OutOfMemoryError 异常。

堆内存用于存放由 new 创建的对象和数组。在堆中分配的内存,由 java 虚拟机 自动垃圾回收器 来管理。在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,在栈中的这个特殊的变量就变成了数组或者对象的引用变量,以后就可以在程序中使用栈内存中的引用变量来访问堆中的数组或者对象,引用变量相当于为数组或者对象起的一个别名,或者代号。

引用变量是普通变量,定义时在栈中分配内存,引用变量在程序运行到作用域外释放。而数组&对象本身在堆中分配,即使程序运行到使用 new 产生数组和对象的语句所在地代码块之外,数组和对象本身占用的堆内存也不会被释放,数组和对象在没有引用变量指向它的时候,才变成垃圾,不能再被使用,但是仍然占着内存,在随后的一个不确定的时间被垃圾回收器释放掉。这个也是 java 比较占内存的主要原因,实际上,栈中的变量指向堆内存中的变量 ,这就是 Java 中的指针!

方法区 (Method Area/PermGen space)

方法区也是各个线程共享的内存区域,它用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区域又被称为 “永久代”,但这仅仅对于 Sun HotSpot 来讲,JRockit 和 IBM J9 虚拟机中并不存在永久代的概念。Java 虚拟机规范把方法区描述为 Java 堆的一个逻辑部分,而且它和 Java Heap 一样不需要连续的内存,可以选择固定大小或可扩展,另外,虚拟机规范允许该区域可以选择不实现垃圾回收。相对而言,垃圾收集行为在这个区域比较少出现。

java 中内存分配策略及堆和栈的比较

内存分配策略

按照编译原理的观点, 程序运行时的内存分配有三种策略, 分别是静态的, 栈式的, 和堆式的.

堆和栈的比较

上面的定义从编译原理的教材中总结而来, 除静态存储分配之外, 都显得很呆板和难以理解, 下面撇开静态存储分配, 集中比较堆和栈:

从堆和栈的功能和作用来通俗的比较, 堆主要用来存放对象的,栈主要是用来执行程序的. 而这种不同又主要是由于堆和栈的特点决定的:

在编程中,例如 C/C++ 中,所有的方法调用都是通过栈来进行的, 所有的局部变量, 形式参数都是从栈中分配内存空间的。实际上也不是什么分配, 只是从栈顶向上用就行, 就好像工厂中的传送带 (conveyor belt) 一样, Stack Pointer 会自动指引你到放东西的位置, 你所要做的只是把东西放下来就行. 退出函数的时候,修改栈指针就可以把栈中的内容销毁. 这样的模式速度最快, 当然要用来运行程序了. 需要注意的是, 在分配的时候, 比如为一个即将要调用的程序模块分配数据区时, 应事先知道这个数据区的大小, 也就说是虽然分配是在程序运行时进行的, 但是分配的大小多少是确定的, 不变的, 而这个 “大小多少” 是在编译时确定的, 不是在运行时.

堆是应用程序在运行的时候请求操作系统分配给自己内存,由于从操作系统管理的内存分配, 所以在分配和销毁时都要占用时间,因此用堆的效率非常低. 但是堆的优点在于, 编译器不必知道要从堆里分配多少存储空间,也不必知道存储的数据要在堆里停留多长的时间, 因此, 用堆保存数据时会得到更大的灵活性。事实上, 面向对象的多态性, 堆内存分配是必不可少的, 因为多态变量所需的存储空间只有在运行时创建了对象之后才能确定. 在 C++ 中,要求创建一个对象时,只需用 new 命令编制相关的代码即可。执行这些代码时,会在堆里自动进行数据的保存. 当然,为达到这种灵活性,必然会付出一定的代价: 在堆里分配存储空间时会花掉更长的时间! 这也正是导致我们刚才所说的效率低的原因。

JVM 中的堆和栈

JVM 是基于堆栈的虚拟机. JVM 为每个新创建的线程都分配一个堆栈. 也就是说, 对于一个 Java 程序来说,它的运行就是通过对堆栈的操作来完成的。堆栈以帧为单位保存线程的状态。JVM 对堆栈只进行两种操作: 以帧为单位的压栈和出栈操作。

我们知道, 某个线程正在执行的方法称为此线程的当前方法. 我们可能不知道, 当前方法使用的帧称为当前帧。当线程激活一个 Java 方法, JVM 就会在线程的 Java 堆栈里新压入一个帧。这个帧自然成为了当前帧. 在此方法执行期间, 这个帧将用来保存参数, 局部变量, 中间计算过程和其他数据. 这个帧在这里和编译原理中的活动纪录的概念是差不多的.

从 Java 的这种分配机制来看, 堆栈又可以这样理解: 堆栈 (Stack) 是操作系统在建立某个进程时或者线程 (在支持多线程的操作系统中是线程) 为这个线程建立的存储区域,该区域具有先进后出的特性。

每一个 Java 应用都唯一对应一个 JVM 实例,每一个实例唯一对应一个堆。应用程序在运行中所创建的所有类实例或数组都放在这个堆中, 并由应用所有的线程共享. 跟 C/C++ 不同,Java 中分配堆内存是自动初始化的。Java 中所有对象的存储空间都是在堆中分配的,但是这个对象的引用却是在堆栈中分配, 也就是说在建立一个对象时从两个地方都分配内存,在堆中分配的内存实际建立这个对象,而在堆栈中分配的内存只是一个指向这个堆对象的指针 (引用) 而已。

总结

栈与堆都是 Java 用来在 RAM 中存放数据的地方。与 C++ 不同,Java 自动管理栈和堆,程序员不能直接地设置栈或堆。

Java 的堆是一个运行时数据区, 类的 (对象从中分配空间。这些对象通过 new、newarray、anewarray 和 multianewarray 等指令建立,它们不需要程序代码来显式的释放。堆是由垃圾回收来负责的,堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,Java 的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。

栈的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。栈中主要存放一些基本类型的变量 (,int, short, long, byte, float, double, boolean, char) 和对象句柄。

-->