jvm 内存溢出 - 方法栈溢出

Java 程序中堆栈( Stack Space )用来做方法的递归调用时压入栈帧( Stack Frame ),每个线程都有自己的堆栈,这个堆栈不是来自 Heap 的分配。所以堆栈的大小不会受到 -Xmx-Xms 的影响,这2个 JVM 参数仅仅是影响 Heap 的大小。虚拟机参数 -Xss 用来指定每个线程的堆栈大小,设定该值后,当递归调用太深的时候,就有可能耗尽堆栈空间,爆出 StackOverflow 的错误,我们使用如下代码做实验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class StackSof {
private static int stackLenth = 0;

private static void incrStackLen() {
stackLenth++;
incrStackLen();
}

public static void main(String[] args) {
try {
incrStackLen();
} catch (Throwable t) {
t.printStackTrace();
}
System.out.println("final stackLenth is:" + stackLenth);
}
}
```

在命令行运行 `java -Xss256k StackSof` 发现控制台很快就会出现期望的错误

``` console
java.lang.StackOverflowError
at StackSof.incrStackLen(StackOom.java:6)
......
......
at StackSof.incrStackLen(StackOom.java:6)
final stackLenth is:2582
```

另外,我们分别设置 `Xss` 值为 `256k`、`512k`、`1m`、`2m`、`4m`、`8m` ,控制台打印的 `final stackLenth` 逐渐变大,说明这个参数确实影响到了方法递归调用深度

| 设定Xss值 | 256k | 512k | 1m | 2m | 4m | 8m |
|:-----|:-----|:-----:|:-----:|:-----:|:-----:|:-----:|
| 最终栈深度 | 2582 | 8957 | 22154 | 70079 | 100666 | 462391 |

另外一种情况,通过不断创建线程产生 OutOfMemoryError 异常,但是这样产生的内存溢出异常与栈空间是否足够大并不存在任何联系,反而是为每个线程的栈分配的内存越大,越容易产生内存溢出。如下代码,执行 `java -Xss8m StackOom` 理应比 `java -Xss4m StackOom` 更容易出现异常。

``` java
public class StackOom {
public static void main(String[] args) {
int count = 0;
try {
while (true) {
new Thread(() -> {
while (true) {
}
}).start();
count++;
}
} catch (Throwable t) {
System.out.println("final threadCount is:" + count);
t.printStackTrace();
}
}
}

操作系统分配给每个进程的内存是有限制的,譬如 32 位的 Windows 限制为 2 GB。虚拟机提供了参数来控制 Java 堆和方法区的这两部分内存的最大值。剩余的内存为 2GB(操作系统限制)减去 Xmx(最大堆容量),再减去 MaxPermSize(最大方法区容量),程序计数器消耗内存很小,可以忽略掉。如果虚拟机进程本身耗费的内存不计算在内,剩下的内存就由虚拟机栈和本地方法栈“瓜分”了。每个线程分配到的栈容量越大,可以建立的线程数量自然就越少,建立线程时就越容易把剩下的内存耗尽。需要注意的是,运行上面代码有可能将操作系统搞死,所以运行前请一定要保持当前的工作,推荐个保险的办法,使用 daocloud 提供的胶囊机,这样就不怕系统挂了^_^,笔者在胶囊机里的运行这段代码,程序跑了近1小时也没出现内存溢出,不过系统确实已经越来越卡了,或许在 docker 容器中运行并限定容器的内存大小更容易出现该异常。

说明

文章摘自《深入理解Java虚拟机》第二版 周志明著,仅作为学习记录,书籍中用到的案例代码及描述有部分修改,但未改变原意。