JAVA栈溢出

Java栈溢出小记

今天偶然有人问起如何在编写Java代码使其在运行时抛出栈溢出异常,看似简单的问题涉及到了Java虚拟机的知识,特记录于此文。

Java虚拟机结构简介

根据《Java虚拟机规范》(The Java Virtual Machine Specification)对于Java虚拟机运行时数据区域(Run-Time Data Areas)的描述,虚拟机运行时的描述,其构成图如下所示:
图中,PC寄存器、Java虚拟机栈及本地方法栈为各线程私有,方法区(包括运行时常量取)及堆为线程间共享的存储空间。针对问题提出的栈溢出,有两个区域与其相关,包括Java虚拟机栈及本地方法栈。查阅《Java虚拟机规范》,针对栈溢出有如下两段描述:
对于Java虚拟机栈

The following exceptional conditions are associated with Java Virtual Machine stacks:

  • If the computation in a thread requires a larger Java Virtual Machine stack than is permitted, the Java Virtual Machine throws a StackOverflowError.
  • If Java Virtual Machine stacks can be dynamically expanded, and expansion is attempted but insufficient memory can be made available to effect the expansion, or if insufficient memory can be made available to create the initial Java Virtual Machine stack for a new thread, the Java Virtual Machine throws an OutOfMemoryError.

对于本地方法栈

The following exceptional conditions are associated with native method stacks:

  • If the computation in a thread requires a larger native method stack than is permitted, the Java Virtual Machine throws a StackOverflowError.
  • If native method stacks can be dynamically expanded and native method stack expansion is attempted but insufficient memory can be made available, or if insufficient memory can be made available to create the initial native method stack for a new thread, the Java Virtual Machine throws an OutOfMemoryError.

由此可见对于Java虚拟机栈与本地方法栈都定义了相似的两种溢出:

  1. 线程请求栈上分配内存时,内存不足:此溢出一般出现在线程递归调用方法时。在线程调用方法时虚拟机创建栈帧保存方法调用信息,在方法调用完成后销毁栈帧释放存储,如果在方法调用过程中无法创建栈帧则会报出StackOverflowError异常。
  2. 动态扩展栈或线程创建时无法分配足够内存:此溢出一般出现在创建新的线程时。创建新的线程,需要在栈上为其分配存储,如果此时栈上存储不足以分配则会报出OutOfMemoryError异常。

代码实现

以下代码在Mac版JDK8中实现及运行,由于HotSpot实现中没有分Java虚拟机栈及本地方法栈[1],故以下代码只针对Java虚拟机栈。Hotspot中设置栈容量的参数为-Xss,后续实验均设置-Xss1M,使用Junit4进行测试

分配栈帧失败(StackOverflowError)

代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class StackOverflow {

public void callMyself(int depth) {
System.out.println(depth);
callMyself(++depth);
}
}

public class StackOverflowTest {
@Test
public void callMyself() throws Exception {
StackOverflow overflow = new StackOverflow();
overflow.callMyself(0);
}

}

最终会抛出java.lang.StackOverflowError,且最终能够达到的栈深度主要与栈内存最大大小与栈帧中局部变量占用的空间有关。使用如下代码最大深度会明显变小

1
2
3
4
5
6
7
8
public class StackOverflow {

public void callMyself(int depth) {
int a,b,c,d,e,f,g,h,i,j,k;
System.out.println(depth+"|");
callMyself(++depth);
}
}

为线程分配栈上内存失败(OutOfMemoryError)

代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class OutOfMemory {
public void createThread() {
while (true) {
Thread t = new Thread(() -> {
while (true) {
System.out.println(System.currentTimeMillis());
}
});
t.start();
}
}
}

public class OutOfMemoryTest {
@Test
public void createThread() throws Exception {
OutOfMemory outOfMemory = new OutOfMemory();
outOfMemory.createThread();
}
}

最终会抛出OutOfMemoryError。

针对于OutOfMemoryError的补充

在HotSpot虚拟机实现中,对于Java线程的创建是映射到操作系统线程中的,如果无法创建操作系统线程也会抛出异常,具体为:java.lang.OutOfMemoryError: unable to create new native thread
通过实验在MacOS中,一般小于2048(多次测试为2023),因为默认Mac每个进程最多分配的线程数为2048。可使用sysctl kern.num_taskthreads命令进行查询。如果需要突破限制可以参考官方解决方案
在centOS中实验发现max_user_processesstack size参数都会限制各进程的线程数量。

参考文献
[1] 周志明.深入理解Java虚拟机[M].北京:机械工业出版社,53