Java - 局部变量和StackOverflowError

本文目的

对Java栈内存进行简单学习总结,并了解 -Xss JVM参数的配置,学会在代码中尽量减少不必要的局部变量声明,从而提高程序效率和编码水平。

Java栈内存简介

Java栈内存空间中主要存放的是局部变量,包括基本数据类型(intshortbytelongfloatdoublecharboolean)和引用数据类型。例如:int a = 1 或者 double x = 0.01 这类代码声明的变量将会直接存放在栈空间中;而 Date today = new Date() 则会将引用对象 today 放在内存中,它引用的真正对象 Date() 则会存放在空间中,本文只讨论栈内存,不讨论堆内存。

引用对象类型可能是一个指向对象起始地址的引用指针,也可能是一个指向代表对象的句柄或其他与此对象相关的位置和 returnAddress 类型(指向了一条字节码指令的地址)。

64位的 longdouble 会占用 2 个局部变量空间,其他类型只占用 1 个局部变量空间。

StackOverFlowError 什么时候会发生

如果我们在一段程序里面分配大量的局部变量,就可能造成栈内存空间不足,引发 java.lang.StackOverFlowError 错误。要模拟一个 StackOverFlowError 错误,最简单的方法就是使用递归。

那么,怎么设置这个栈内存空间的大小呢,那就是 -Xss 参数。

-Xss参数的配置

-Xss 参数用来设置栈内存空间的大小,例如 -Xss128K 指分配 128K 的栈内存大小。为什么是 128K 而不是 1K 或者 10K 呢?这个数值的大小可以随便设置吗?

我们不妨尝试一下,随便写一个含有 main 方法的Java程序,然后在 IDEA 中运行,指定 VM参数-Xss10K,如下图所示:

IDEA设置-Xss参数
IDEA设置-Xss参数

运行程序,会得到如下错误(注意如果设置为1K,该参数可能不会生效而是采用初始的栈内存大小):

1
2
3
Error: Could not create the Java Virtual Machine.
The stack size specified is too small, Specify at least 104k
Error: A fatal exception has occurred. Program will exit.

可以看出,JVM对于栈内存的大小是有最低要求的,不能低于 104K。经测试,当设置稍微低于 104K 的时候,程序有时候也是可以运行的,但尽量不要这样做。

下面,就通过程序代码来实战 StackOverFlowError 错误。

JavaXssDemo1

这个例子中,递归方法体里面有 x1x2 两个局部变量。加入 -Xss104k JVM参数,运行以下程序:

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
/**
* Java - 栈内存大小设置Demo1
*
* @author Zebe
*/
public class JavaXssDemo1 {

/**
* 递归深度
*/
private static int count = 0;

/**
* 递归测试(包含少量局部变量)
*/
private static void recursionWithFewVariables() {
long x1 = 1, x2 = 2;
count++;
recursionWithFewVariables();
}

/**
* 程序入口
* -Xss104k
*
* @param args 运行参数
*/
public static void main(String[] args) {
try {
recursionWithFewVariables();
} catch (Throwable e) {
System.out.println("递归测试(包含少量局部变量long),调用深度 = " + count);
e.printStackTrace();
}
}

}

程序输出结果如下:

1
2
3
递归测试(包含少量局部变量),调用深度 = 785
java.lang.StackOverflowError
at me.zebe.cat.java.jvm.JavaXssDemo1.recursionWithFewVariables(JavaXssDemo1.java:19)

JavaXssDemo2

这个例子中,,将 JavaXssDemo1 中的局部变量从 2 个增加到 10 个。加入 -Xss104k JVM参数,运行以下程序:

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
/**
* Java - 栈内存大小设置Demo2
*
* @author Zebe
*/
public class JavaXssDemo2 {

/**
* 递归深度
*/
private static int count = 0;

/**
* 递归测试(包含多个局部变量)
*/
private static void recursionWithMoreVariables() {
long x1 = 1, x2 = 2, x3 = 3, x4 = 4, x5 = 5, x6 = 6, x7 = 7, x8 = 8, x9 = 9, x10 = 10;
count++;
recursionWithMoreVariables();
}

/**
* 程序入口
* -Xss104k
*
* @param args 运行参数
*/
public static void main(String[] args) {
try {
recursionWithMoreVariables();
} catch (Throwable e) {
System.out.println("递归测试(包含多个局部变量long),调用深度 = " + count);
e.printStackTrace();
}
}

}

程序输出结果如下:

1
2
3
递归测试(包含多个局部变量),调用深度 = 363
java.lang.StackOverflowError
at me.zebe.cat.java.jvm.JavaXssDemo2.recursionWithMoreVariables(JavaXssDemo2.java:19)

JavaXssDemo3

这个例子中,将 JavaXssDemo2 中的局部变量类型由 long 改为 int,数量不变 (还是10个)。加入 -Xss104k JVM参数,运行以下程序:

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
/**
* Java - 栈内存大小设置Demo3
*
* @author Zebe
*/
public class JavaXssDemo3 {

/**
* 递归深度
*/
private static int count = 0;

/**
* 递归测试(包含多个局部变量)
*/
private static void recursionWithMoreVariables() {
int x1 = 1, x2 = 2, x3 = 3, x4 = 4, x5 = 5, x6 = 6, x7 = 7, x8 = 8, x9 = 9, x10 = 10;
count++;
recursionWithMoreVariables();
}

/**
* 程序入口
* -Xss104k
*
* @param args 运行参数
*/
public static void main(String[] args) {
try {
recursionWithMoreVariables();
} catch (Throwable e) {
System.out.println("递归测试(包含多个局部变量int),调用深度 = " + count);
e.printStackTrace();
}
}

}

程序输出结果如下:

1
2
3
递归测试(包含多个局部变量),调用深度 = 551
java.lang.StackOverflowError
at me.zebe.cat.java.jvm.JavaXssDemo3.recursionWithMoreVariables(JavaXssDemo3.java:19)

对比及思考

栈空间大小 局部变量类型 局部变量个数 调用深度
104K long 2 785
104K long 10 363
104K int 10 551

通过以上例子可以看出:

  • 在相同的栈内存空间下,局部变量越少,可以递归调用的次数越多
  • 相反,如果有过多的局部变量,则会增加栈内存的开销

同时,long 类型是64位,它会占用 2 个局部变量空间,而 int 占用的是 1 个局部变量空间。

因此,我们应当在编写程序的过程中,合理地使用栈空间,尽量减少不必要的局部变量分配,特别是在递归方法中尤其要谨慎使用局部变量