写项目时,内存溢出、OutOfMemoryError、StackOverflowError 是很常见的bug。字面意思很明白,比如某个对象存储需要100M,但栈空间只剩下80M,这时程序就会抛出内存溢出之类的异常。解决措施也很多,比如变量使用后将其设为null,或者调用System.gc() 。说实话这些解决方式使用时有些盲目,我觉得理解清楚堆栈以及内存分配是解决这类问题的第一步。

首先申明这篇文章我不是首创,只是觉得原作者很好地解释了我的疑问。这里我做下记录(原文章是英文,文章最后我会配上链接),希望可以帮助更多人理解它们之间的关系。

堆(Java Heap Space)

Java堆空间由java运行时使用,用来给对象和类分配内存。任何java对象,都是在堆空间里被创建。

垃圾回收机制在栈中运行,用来释放栈中没被引用的对象所占的内存。在堆空间创建的对象全局可访,并且在应用的任何地方都可被引用。

栈(Java Stack Memory)

栈用来运行一个线程,栈内会含有一个方法的局部变量和引用变量。栈内存遵循后进先出(Last-In-First-Out) 原则,当一个方法被调用,一个新的区块就在栈里创建了,用来存储该方法所使用的局部变量和引用。方法调用结束后,该区块废弃了,并且可被下一个方法使用。

栈的空间大小远小于堆。

程序中的堆栈

下面用一段简单的程序来理解下堆栈内存的使用

package com.journaldev.test;
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设置堆栈空间 java堆栈大小_java

一旦程序运行起来,所有运行时的类都被加载进堆空间。执行到main()(Line 1)方法,java 运行时会创建栈内存,供main()方法所在的线程使用。

Line 2,我们在main()方法的栈内存里生成,并存储了原始数据类型的局部变量。

Line 3,因为我们创建了对象,所以它被存储在堆空间。与此同时,栈里也包含了该对象的引用。

Line 4,我们创建了Memory对象,该对象也是被存储于堆空间,同时栈里也包含该对象的引用。

在Line 5,我们调用了foo()方法。此时会在栈顶创建一个新的区块,用来被foo()方法所在的线程使用。同时因为传入了Object对象,foo()对应的栈空间会创建Object对象的引用(Line 6)。

Line 7,字符串被创建,该字符串进入String Pool。同时,foo()对应的栈空间会创建该字符串的引用。

Line 8,foo()方法执行完毕。此时,栈里分配给该方法所在线程的内存会被释放掉。

Line 9,main()方法执行完毕,此时它所占有的栈内存被释放。同样地,整个程序也在此行结束,java运行时释放所有占用的内存,并结束程序的运行。

堆栈间的关系

基于上述的逐条说明,我们可以得出以下结论:堆空间可以被整个应用使用,然而栈内存只能被一个正在执行的线程使用。

不管什么时候创建对象,它总是被存储在堆空间里,栈只会含有它的引用。栈内存只会含有原始数据类型的局部变量,以及对堆里对象的引用。

栈里的对象都是全局可访问的,然而对空间不能被其他线程访问。

栈的内存管理满足后进先出院子,而堆空间因为全局可用,管理起来会复杂的多。堆空间可以划分为 Young-Generation,Old-Generation 等等。具体参见垃圾回收机制。

栈内存生命周期很短,线程执行结束其对应的栈空间也就释放掉了。而堆空间从程序开始运行到结束,会一直存在。

我们可以使用-Xms和-Xmx这两个jvm选项定义堆内存的区间,使用-Xss定义栈内存。

栈空间不足,java 运行时会抛出java.lang.StackOverFlowError异常。堆空间不足,则抛出java.lang.OutOfMemoryError: Java Heap Space异常。

栈内存比堆内存小得多。由于内存分配简单(LIFO),栈的运行速度比堆更快。