admin管理员组文章数量:1441565
Java 中的堆内存和堆栈内存进阶
概述
堆栈内存是一种静态内存分配方案,其中存储了所有函数调用、特定于方法的原始数据值以及对堆内存中对象的引用。对堆栈内存的访问按后进先出 (LIFO) 顺序进行。
堆内存用于动态内存分配在执行 Java 程序期间创建的 Java 对象和 JRE 类。堆内存在运行时分配给对象,这些对象具有全局访问权限,这意味着可以从应用程序中的任何位置访问它们。
本文旨在:
- 解释 Java 中的堆栈和堆内存。
- 在 Java 中讨论 OutOfMemoryError 和 StackOverflowError。
- 阐明堆栈和堆内存的优缺点。
介绍
Java应用程序在计算机的RAM中编译和执行。每个应用程序都分配有一定量的内存。RAM 中分配的内存称为应用程序内存。分配给 Java 进程的内存量取决于多种因素,如 Java 运行时环境 (JRE)、操作系统、处理器等。
JVM 中的内存分为五个部分:
- Stack
- Heap
- Class/Method Area
- Program Counter Register
- Native Method Stack
在本文中,我们将深入探讨 Java 中的堆栈和堆内存,若想快速了解本主题请参阅文章Java 中的堆栈内存和堆空间介绍-Java快速进阶教程
什么是 Java 中的堆栈内存?
堆栈内存是一种静态内存分配方案,其中存储了所有函数调用、特定于方法的原始数据值以及对堆内存中对象的引用。堆栈内存始终以后进先出 (LIFO) 方式访问。
调用方法时堆栈内存中的更改:
- 在堆栈内存中,为每个调用的方法创建一个新的内存块。特定于该方法的所有基元数据值以及对从该方法引用的对象的引用都存储在此内存块中。
- 当该方法完成其执行时,将从堆栈内存中清除内存块,并且堆栈内存可供使用。
- 只要创建它们的函数在执行中,堆栈中的值就存在。堆栈内存的大小是固定的,一旦创建就无法增长或缩小。
Java 中的堆栈内存示例
下面是一个简单的Java程序,有三个方法main,addOne和addTwo。当该程序执行时,我们将看到堆栈用法的分步说明。
代码语言:javascript代码运行次数:0运行复制public class Main {
public static int addOne(int input) {
return input + 1;
}
public static int addTwo(int input) {
return input + 2;
}
public static void main(String[] args) {
int x = 0;
x = addOne(x);
x = addTwo(x);
}
}
- 当程序被执行时,主方法首先由JVM执行。调用 main 方法时,将在调用堆栈中为其分配一个块。
- main 方法包含一个基元值 x。此基元值存储在为 main 方法分配的内存块中。
- 当从主方法调用 addOne 方法时,将在堆栈内存中为 addOne 方法分配一个新块。
- 特定于该方法的变量将创建并存储在分配的内存块中。方法执行完成后,该值将返回给调用方法(此处为 main 方法),并将块从调用堆栈中删除。
- 同样,当调用 addTwo 方法时,会为其分配一个新块,并创建和存储变量。方法完成执行后,该值将返回到调用方法,并清除块。
- 最后,main 方法完成其执行,并且从堆栈中清除 main 方法对应的内存块。
什么是Java中的StackOverflowError?
如前所述,堆栈内存的大小有限,一旦创建就无法放大或缩小。因此,如果我们使用所有的堆栈内存,将没有空间用于即将到来的方法调用,我们将得到 StackOverflowError。
法典:
代码语言:javascript代码运行次数:0运行复制public class Main {
public static int factorial(int n) {
/* Base case is commented to make it run indefinitely
if (n == 0) {
return 1;
}
*/
return n * factorial(n - 1);
}
public static void main(String[] args) {
int x = 3;
int fact = factorial(x);
}
}
错误:
代码语言:javascript代码运行次数:0运行复制Exception in thread "main" java.lang.StackOverflowError
at StackMemory.factorial(StackMemory.java:15)
at StackMemory.factorial(StackMemory.java:15)
...
解释:
- 堆栈上的每个调用都会为阶乘方法分配一个新块。
- 当执行上述程序时,阶乘方法将被无限期调用,因为基本情况被注释。
- 由于堆栈大小是固定的,并且阶乘方法被无限期调用并且不返回任何值,因此堆栈内存耗尽,从而导致 StackOverflowError。
什么是 Java 中的堆内存?
堆内存用于存储 Java 程序执行期间创建的对象。对在堆中创建的对象的引用存储在堆栈内存中。堆遵循动态内存分配方案(内存在执行期间或运行时分配),并提供对所创建对象的随机和全局访问,这与堆栈不同,堆栈遵循后进先出 (LIFO) 内存访问顺序。
与堆栈相比,堆内存的大小很大。堆内存中未使用的对象由 Java 中的垃圾回收器自动清除。
堆内存可以分为三个部分:
- 新一代或年轻一代
- 老一代或终身一代
- 永久生成
新一代或年轻一代
年轻一代是分配所有新创建对象的内存空间。新一代依次有3个部分,伊甸园,幸存者1和幸存者2。所有新创建的对象都分配在伊甸园空间中。当伊甸园已满时,会发生一次较小的垃圾回收,活动对象将移动到幸存者 1,然后移动到幸存者 2。
幸存者 1 和幸存者 2 包含在次要垃圾回收中幸存下来的对象。在伊甸园、幸存者1和幸存者2中幸存下来的物体被移到老一代。在“旧一代”中,垃圾的收集频率较低,因此使用“幸存者 1”和“幸存者 2”空间来确保只有长期存活的对象才会移动到永久生成。
老一代或终身一代
年龄是为年轻一代分配的对象设定的。当那个年龄到来时,这些活物被转移到老一代。通常,长期存活的物体存储在老一代中。一个主要的垃圾收集在老一代上运行,以收集死物。
永久生成
JVM 使用永久生成来存储有关类和方法的元数据。JVM还以永久生成的方式存储Java标准库。此空间作为完整垃圾收集的一部分进行清理。
Java 中的堆内存示例
代码语言:javascript代码运行次数:0运行复制import java.util.ArrayList;
import java.util.List;
public class HeapMemory {
public static void main(String[] args) {
int x = 10;
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
}
}
解释:
在上面的示例中,变量 x 在堆栈中分配,而对象列表在堆中分配内存。只有对列表对象的引用与 x 一起存储在堆栈内存中。
为什么在 Java 中抛出 OutOfMemoryError?
当堆中没有更多空间来创建和存储新对象时,将引发 OutOfMemoryError。当垃圾回收器无法释放任何空间来存储新对象时,会发生这种情况。
代码语言:javascript代码运行次数:0运行复制public class HeapMemory {
public static void main(String[] args) {
for (int i = 1; i < 100; i *= 2) {
int n = (int) Math.pow(2, i);
int[] array = new int[n];
for (int j = 0; j < n; j++) {
array[j] = 1000;
}
}
}
}
错误
代码语言:javascript代码运行次数:0运行复制Exception in thread "main" java.lang.OutOfMemoryError: Requested array size exceeds VM limit
at HeapMemory.main(HeapMemory.java:7)
解释:
考虑上面的程序,我们反复生成更大大小的数组并在其中存储值。一旦堆中的空间用完,它就会抛出 OutOfMemoryError。
堆栈内存和堆内存之间的差异
Property | 堆栈内存 | 堆内存 |
---|---|---|
Size | 堆栈内存的大小更小 | 堆内存的大小更大 |
Order | 堆栈内存以后进先出 (LIFO) 方式访问 | 堆内存是动态分配的,不遵循任何顺序 |
Speed | 由于采用后进先出 (LIFO) 排序,对堆栈内存的访问速度更快 | 对堆内存的访问速度较慢,因为它不遵循任何顺序并且是动态分配的 |
Resizing | 堆栈中不允许调整变量的大小 | 允许在堆中调整变量的大小 |
Resizing | 当方法分别开始和完成其执行时,会自动分配和释放内存 | 创建对象时分配内存,当对象不再使用时由垃圾回收器解除分配内存 |
Storage | 特定于方法的基元值和来自该方法的对象引用存储在堆栈中 | 新创建的对象和 JRE 类存储在堆中 |
Exception | 当堆栈中没有更多空间用于新方法调用时,将引发 StackOverflowError | 当堆中没有剩余空间来分配新对象时,会引发内存不足错误 |
Thread Safety | 每个线程都分配有一个新的堆栈,它是线程安全的 | 堆内存在所有线程之间共享,并且不是线程安全的 |
Scope | 堆栈内存仅由一个执行线程使用。 | 堆内存由应用程序的所有部分使用。 |
Lifetime | 堆栈内存的生存期很短。 | 堆内存从应用程序执行的开始到结束一直存在。 |
在 Java 中使用堆栈内存的优势
- 堆栈内存是线程安全的,因为每个线程都有其堆栈区域。
- 内存分配和释放速度更快。
- 为方法自动分配和取消分配内存。
- 对堆栈内存的访问速度更快。
在 Java 中使用堆栈内存的缺点
- 堆栈内存是固定的,一旦创建就无法增长或收缩。
- 堆栈内存以后进先出 (LIFO) 方式工作。因此,随机访问是不可能的。
- 堆栈内存既不可扩展也不灵活。
在 Java 中使用堆内存的优点
- 堆内存不是固定的,它可以增长和收缩。
- 堆内存不进行任何排序,并允许随机访问。
- 堆内存是可扩展且灵活的。
- 可以从应用程序中的任何位置访问在堆内存中创建的对象。
在 Java 中使用堆内存的缺点
- 堆内存在线程之间共享,不是线程安全的。
- 对堆内存的访问速度较慢。
- 与堆栈相比,内存分配和释放很复杂。
结论
- 堆栈和堆内存由 Java 虚拟机 (JVM) 分配给应用程序。
- Java 中的堆栈内存用于执行线程和静态内存分配。
- 堆栈内存包含特定于方法的基元值和对堆中从方法引用的对象的引用。
- 堆栈内存按后进先出 (LIFO) 顺序访问。
- 堆栈内存的大小很小且固定。
- 当变量的作用域未在方法作用域之外使用时,可以使用堆栈,对内存的访问应该更快,并且变量应该是线程安全的。
- 创建的所有对象都存储在堆中。
- 与堆栈相比,堆的大小很大。
- 堆内存遵循动态分配,分为三个部分:新一代、年轻一代和永久一代。
- 当作用域是全局的并且需要跨线程访问对象时,可以使用堆内存
本文标签: Java 中的堆内存和堆栈内存进阶
版权声明:本文标题:Java 中的堆内存和堆栈内存进阶 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/biancheng/1747892849a2772781.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论