深入理解Java内存管理:栈内存与堆内存的区别及应用场景

Java把内存分成两种,一种叫做栈内存,一种叫做堆内存

深入理解Java内存管理:栈内存与堆内存的区别及应用场景

在函数里定义的一些基本类型的变量,其在函数的栈内存中分配。在函数里定义的对象的引用变量,也在函数的栈内存中分配。当在一段代码块中定义一个变量时,Java 会在栈中为这个变量分配内存空间。当变量的作用域结束后,Java 会自动释放为该变量分配的内存空间,而这个内存空间可以立刻被用于其他用途。

堆内存会用来存放通过 new 创建出来的对象以及数组。在堆当中进行分配的内存,是由 java 虚拟机所自带的自动垃圾回收器来进行管理的。产生数组或对象于堆中后,可在栈中定义一特殊变量,此变量取值等于数组或对象在堆内存中的首地址,栈中的该特殊变量便成为数组或对象的引用变量,之后可在程序中用栈内存中的引用变量访问堆中的数组或对象,引用变量就如同为数组或对象起的别名或代号。

引用变量属于普通变量,在定义时会在栈中分配内存。并且,引用变量会在程序运行到其作用域之外时被释放。数组和对象在堆中进行分配。当程序运行至使用 new 生成数组和对象的语句所在的代码块之外时,数组和对象本身所占据的堆内存不会被释放。只有在数组和对象没有引用变量指向它的时候,才会变成垃圾,无法再被使用,不过依然占据着内存,会在随后某个不确定的时间被垃圾回收器释放掉。这个是 Java 比较占内存的主要原因之一。实际上,栈里的变量会指向堆内存中的变量,而这就是 Java 中的指针。

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

1 内存分配策略

从编译原理的角度来看,程序在运行时进行内存分配存在三种不同的策略。其一为静态的内存分配策略;其二是栈式的内存分配策略;其三是堆式的内存分配策略。

静态存储分配意味着在编译阶段就能明确每个数据目标在运行时刻所需的存储空间。所以在编译时可以给它们分配固定的内存空间。这种分配策略要求程序代码中不能有可变数据结构,比如可变数组。同时也不允许有嵌套或者递归的结构出现,因为这些情况都会使编译程序无法计算出准确的存储空间需求。

栈式存储分配也叫动态存储分配。它是通过一个类似堆栈的运行栈来实现的。与静态存储分配不同,在栈式存储方案中,程序对数据区的需求在编译时是完全未知的,只有到运行时才能知晓。不过规定,在运行中进入一个程序模块时,必须先知道该程序模块所需的数据区大小,然后才能为其分配内存。就像我们在数据结构中熟知的栈一样,栈式存储分配按照先进后出的原则进行分配。

堆中的内存可按任意顺序进行分配和释放。

2 堆和栈的比较

上面的定义是从编译原理的教材中总结出来的。除了静态存储分配之外,其他的都显得很呆板并且难以理解。下面我们撇开静态存储分配,集中来比较堆和栈。

从功能和作用方面通俗地比较堆和栈,堆主要是用于存放对象的,栈主要是用于执行程序的。并且这种不同主要是由堆和栈的特点所决定的:

在编程里,像 C/C++这类语言中,所有的方法调用是依靠栈来实现的。并且,所有的局部变量以及形式参数,都是从栈里分配内存空间的。实际上并非是分配,只需从栈顶向上使用即可,就如同工厂中的传送带那样。Stack Pointer 会自动引导你到达放置东西的位置,你只需将东西放下来就行。在退出函数时,通过修改栈指针就能销毁栈中的内容。这种模式的速度是最快的,当然可以用来运行程序。需要留意的是,在进行分配时,比如为一个即将要调用的程序模块分配数据区,应该事先知晓这个数据区的大小,也就是说,虽然分配是在程序运行时进行的,但分配的大小是确定且不变的,而这个“大小多少”是在编译时确定的,并非在运行时。

应用程序在运行时会请求操作系统分配内存给堆。因为是从操作系统管理的内存进行分配,所以在分配和销毁时都需要占用时间,这就导致用堆的效率很低。然而,堆的优点在于,编译器无需知道要从堆里分配多少存储空间,也不用知道存储的数据要在堆里停留多久,所以,在使用堆保存数据时能获得更大的灵活性。事实上,面向对象的多态性需要堆内存分配。因为只有在运行时创建了对象之后,多态变量所需的存储空间才能确定。在 C++中,创建一个对象时,只需用 new 命令编制相关代码即可。执行这些代码时,数据会在堆里自动被保存。当然,要达到这种灵活性,是需要付出一定代价的。在堆里分配存储空间时会花费更长的时间!这就是导致我们刚才所说效率低的原因。看来正如列宁同志所说,人的优点往往也是人的缺点,人的缺点往往也是人的优点。(晕~)

3 JVM中的堆和栈

JVM是基于堆栈运行的虚拟机。每个新创建的线程,JVM 都会为其分配一个堆栈。对于一个 Java 程序而言,其运行是依靠对堆栈的操作来实现的。堆栈是以帧为单位来保存线程状态的。JVM 对堆栈的操作主要有两种,即以帧为单位的压栈操作和出栈操作。

我们知道,某个线程正在执行的那个方法,被称作此线程的当前方法。我们或许不知道,当前方法所使用的那个帧,被叫做当前帧。当线程将一个 Java 方法激活时,JVM 就会在该线程的 Java 堆栈里新压入一个帧。这个帧成为了当前帧。在该方法执行过程中,此帧会被用于保存参数、局部变量、中间计算过程以及其他数据。此帧与编译原理中的活动纪录概念相近。

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

Java 应用与 JVM 实例一一对应,每个 JVM 实例又对应一个堆。应用程序运行时创建的所有类实例和数组都置于这个堆中,且被应用的所有线程所共享。与 C/C++不同,Java 中分配堆内存是会自动初始化的。Java中所有对象的存储空间于堆中进行分配。然而,这个对象的引用是在堆栈中分配的。这意味着在创建一个对象时,会从两个地方分配内存。在堆中分配的内存用于实际建立这个对象,而在堆栈中分配的内存仅仅是一个指向这个堆对象的指针(引用)。

Java 中的堆和栈

Java把内存划分成两种:一种是栈内存,一种是堆内存。

函数中定义的对象的引用变量在函数的栈内存中分配。

在一段代码块中定义一个变量时,Java 会在栈里为这个变量分配内存空间。当变量的作用域结束后,Java 会自动把为该变量所分配的内存空间释放掉,而这个内存空间能够立刻被用于其他用途。

堆内存用来存放由new创建的对象和数组。

在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。

产生数组或对象于堆中之后,能够在栈中定义一个特殊的变量,使栈中该变量的取值与数组或对象在堆内存中的首地址相等,如此一来,栈中的这个变量便成为了数组或对象的引用变量。

引用变量就像是给数组或对象起的一个名字。之后,在程序里能够利用栈中的引用变量去访问堆中的数组或对象。

具体的说:

程序员不能直接对栈或堆进行设置。

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

栈主要用来存放一些基本类型的变量,比如 int、short、long、byte、float、double、boolean、char 等,以及对象句柄。

栈具有一个极为重要的特殊性,即存在于栈中的数据能够被共享。倘若我们同时进行如下定义:

int a = 3;

int b = 3;

编译器首先处理 int a = 3 。它会在栈中创建一个名为 a 的变量引用。接着查找栈中是否存在 3 这个值。如果没找到,就把 3 存放进去。然后让 a 指向 3 。接着处理 int b = 3 。在创建完 b 的引用变量后,由于栈中已经有 3 这个值,就直接让 b 指向 3 。这样,就出现了 a 和 b 同时都指向 3 的情况。这时,如果令 a 等于 4 。那么编译器会去栈中重新搜索是否存在 4 值。若没有,就会将 4 存放进来,并且让 a 指向 4 。要是已经有了 4 值,就会直接让 a 指向这个地址。所以 a 值的改变不会对 b 的值产生影响。要注意这种数据的共享和两个对象引用同时指向一个对象的共享有所不同。因为在这种情况下,a 的修改不会影响到 b,这种共享是由编译器完成的,它有利于节省空间。而一个对象引用变量若修改了这个对象的内部状态,就会影响到另一个对象引用变量。

你可能想看:
« 上一篇
下一篇 »

相关推荐

java中什么是冒泡,冒泡算法

2026年09月06日

27阅读

java中冒号,java双冒号

2026年09月06日

26阅读

java中的->,java中的是什么意思

2026年09月06日

24阅读

java中的super用法,Java中super的说明

2026年09月06日

28阅读

java中反射的应用,java中反射的应用场景

2026年09月06日

25阅读

java中开关的定义,Java关键字有哪些

2026年09月06日

25阅读