我问这个问题是为了了解如何增加 JVM 中运行时调用堆栈的大小。对于这个问题,我已经有了一个答案,而且我还得到了许多与 Java 如何处理需要大型运行时堆栈的情况相关的有用答案和注释。我已经把我的问题扩展到了回答的摘要。
最初,我想增加 JVM 堆栈的大小,这样程序就可以在没有 StackOverflowError
的情况下运行。
public class TT {
public static long fact(int n) {
return n < 2 ? 1 : n * fact(n - 1);
}
public static void main(String[] args) {
System.out.println(fact(1 << 15));
}
}
相应的配置设置是具有足够大的值的 java -Xss...
命令行标志。对于上面的程序 TT
,它在 OpenJDK 的 JVM 中是这样工作的:
$ javac TT.java
$ java -Xss4m TT
其中一个答案还指出,-X...
标志是依赖于实现的
java version "1.6.0_18"
OpenJDK Runtime Environment (IcedTea6 1.8.1) (6b18-1.8.1-0ubuntu1~8.04.3)
OpenJDK 64-Bit Server VM (build 16.0-b13, mixed mode)
也可以只为一个线程指定一个大堆栈(请参见其中一个答案)。在 java -Xss...
上建议这样做,以避免浪费不需要它的线程的内存。
我很好奇上面的程序到底需要多大的堆栈,所以我把它的 n
增加了:
fact(1 << 15)
fact(1 << 17)
fact(1 << 18)
fact(1 << 19)
fact(1 << 20)
fact(1 << 21)
fact(1 << 22)
fact(1 << 23)
fact(1 << 24)
fact(1 << 25)
从上面的数字可以看出,对于上面的函数,Java 在每个堆栈帧上使用了大约16个字节,这是合理的。
上面的枚举包含 就足够了而不是 就够了,因为堆栈需求是不确定的: 使用相同的源文件多次运行它,相同的 -Xss...
有时会成功,有时会产生一个 StackOverflowError
。例如,对于1 < 20,-Xss18m
在10次运行中的7次运行中是足够的,而 -Xss19m
也不总是足够的,但是 -Xss20m
是足够的(在100次运行中的所有100次运行中)。垃圾收集、 JIT 启动或其他原因导致了这种不确定性行为吗?
在 StackOverflowError
处打印的堆栈跟踪(可能还有其他异常)只显示运行时堆栈的最新1024个元素。下面的答案演示了如何计算到达的确切深度(可能比1024大得多)。
许多回应的人指出,考虑使用相同算法的替代的、不那么需要堆栈的实现是一种良好和安全的编码实践。通常,可以将一组递归函数转换为迭代函数(使用例如 Stack
对象,该对象在堆上而不是在运行时堆栈上填充)。对于这个特定的 fact
函数,转换它非常容易。我的迭代版本应该是这样的:
public class TTIterative {
public static long fact(int n) {
if (n < 2) return 1;
if (n > 65) return 0; // Enough powers of 2 in the product to make it (long)0.
long f = 2;
for (int i = 3; i <= n; ++i) {
f *= i;
}
return f;
}
public static void main(String[] args) {
System.out.println(fact(1 << 15));
}
}
仅供参考,正如上面的迭代解决方案所示,fact
函数不能计算65以上(实际上,甚至超过20)的精确阶乘,因为 Java 内置类型 long
会溢出。重构 fact
,使其返回 BigInteger
而不是 long
,也会产生大输入的精确结果。