对于这个问题,我是这样想的:
我们知道栈大小是可以通过参数(-Xss)设置的,栈是线程私有的,线程内部的每个方法调用会创建一个栈帧,所以如果“栈帧的数量*每个栈帧的大小>栈大小”时便会发生“栈溢出”。
这样的设想基于:
接下来我们将分析一下我们的设想是否正确。
验证程序
//JVM参数:-Xss108k
//对比“一个线程栈溢出”和“两个线程栈溢出”时的栈深度,从而验证设置的栈大小是否只是限制一个线程栈
//大小
private static int c1 = 0;
private static int c2 = 0;
public static void main(String[] args) {
//线程一递归
new Thread(() -> {
try {
m1();
} catch (StackOverflowError error) {
System.out.println("栈溢出 c1=" + c1);
}
}).start();
//线程二递归
new Thread(() -> {
try {
m2();
} catch (StackOverflowError error) {
System.out.println("栈溢出 c2=" + c2);
}
}).start();
}
public static void m1() {
c1++;
m1();
}
public static void m2() {
c2++;
m2();
}
执行结果
两个线程:
一个现场:
总结
从执行结果来看,并没有因为线程个数增加,导致单个线程栈深度降低,所以可以验证第一点正确。
在《深入了解JVM》一书中,介绍到:每一个方法调用时,都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口灯信息。所以这第二点也可验证。
我们知道栈是一种后进先出的数据结构,递归太深,意味着只有栈帧入栈还没等到栈帧出栈,就已经超过栈所能承受的大小。所以这一点也不难理解。
在《深入了解JVM》一书中说到:当线程请求的栈深度超过虚拟机允许的栈深度时,便会抛出StackOverFlowError。这是一个抽象化的总结,我们对此做了如上分析。
线上临时解决办法
重新调整JVM参数-Xss,重启应用
代码层面
将递归改为循环,如上问中的代码可修改为:
new Thread(() -> {
try {
//这里只是模仿死循环。真实业务肯定有循环终止条件的
while(true){
m1();
}
} catch (StackOverflowError error) {
System.out.println("栈溢出 c1=" + c1);
}
}).start();
//去掉递归
public static void m1() {
c1++;
}
在上文线上临时解决方案中,我们通过增大栈大小,使程序继续运行。那为什么我们一开始发布程序的时候就将“栈大小”设置的大一些呢?
首先,操作系统分配给每个进程的内存是有限制的。那么:
可用的栈内存=进程最大内存-堆内存-方法区内存-程序计数器内存-虚拟机本身耗费内存
而栈是线程私有的,那么可以认为:
程序可建立的线程数量=可用栈内存/栈大小
这样当栈大小设置太大时,就会导致创建的线程数量太少。这样在多线程的情况下便可能发生“内存溢出”情况。
在x64位Linux操作系统上,JVM默认的栈大小为1024kb。
由于我们线上的程序要支持高并发场景,所以栈的大小设置为256kb,这里仅供参考。