如何增加 Java 堆栈的大小?

我问这个问题是为了了解如何增加 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增加了:

  • - Xss4m 可以足够 fact(1 << 15)
  • - Xss5m 可以足够 fact(1 << 17)
  • - Xss7m 可以足够 fact(1 << 18)
  • - Xss9m 可以足够 fact(1 << 19)
  • - Xss18m 可以足够 fact(1 << 20)
  • - Xss35m 可以足够 fact(1 << 21)
  • - Xss68m 可以足够 fact(1 << 22)
  • - Xss129m 可以足够 fact(1 << 23)
  • - Xss258m 可以足够 fact(1 << 24)
  • - Xss515m 可以足够 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,也会产生大输入的精确结果。

218773 次浏览

嗯... ... 它对我来说很有用,而且堆栈大小远低于999MB:

> java -Xss4m Test
0

(Windows JDK 7、构建17.0-b05客户端 VM 和 Linux JDK 6——与您发布的版本信息相同)

如果您想使用线程堆栈大小,那么您需要查看 Hotspot JVM 上的-Xss 选项。在非 Hotspot VM 上可能有些不同,因为 JVM 的-X 参数是特定于发行版的,即 IIRC。

在 Hotspot 上,如果你想使尺寸达到16meg,这看起来就像 java -Xss16M

如果希望查看可以传入的所有特定于发行版的 JVM 参数,请键入 java -X -help。我不确定这种方法在其他 JVM 上是否也适用,但它打印了所有特定于 Hotspot 的参数。

值得一提的是,我建议您在 Java 中限制使用递归方法。它不太擅长优化它们——比如 JVM 不支持尾部递归(参见 JVM 阻止尾部调用优化吗?)。尝试重构上面的阶乘代码,使用 while 循环代替递归方法调用。

我想你是通过堆栈跟踪中的循环行计算出“1024的深度”的吧?

显然,堆栈跟踪数组在 Throwable 的长度似乎被限制在1024。 试试下面的程序:

public class Test {


public static void main(String[] args) {


try {
System.out.println(fact(1 << 15));
}
catch (StackOverflowError e) {
System.err.println("true recursion level was " + level);
System.err.println("reported recursion level was " +
e.getStackTrace().length);
}
}


private static int level = 0;
public static long fact(int n) {
level++;
return n < 2 ? n : n * fact(n - 1);
}
}

奇怪! 你是说你想生成一个 1 < 15深度的复发? ? ? ! ! ! !

我建议你不要尝试。堆栈的大小将是 2^15 * sizeof(stack-frame)。我不知道堆栈帧大小是多少,但2 ^ 15是32.768。差不多... 如果它停在1024(2 ^ 10)你就得把它放大2 ^ 5倍它确实是,比你实际设置的大32倍。

控制进程内堆栈大小的唯一方法是启动一个新的 Thread。但是您也可以通过使用 -Xss参数创建一个自调用子 Java 进程来进行控制。

public class TT {
private static int level = 0;


public static long fact(int n) {
level++;
return n < 2 ? n : n * fact(n - 1);
}


public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(null, null, "TT", 1000000) {
@Override
public void run() {
try {
level = 0;
System.out.println(fact(1 << 15));
} catch (StackOverflowError e) {
System.err.println("true recursion level was " + level);
System.err.println("reported recursion level was "
+ e.getStackTrace().length);
}
}


};
t.start();
t.join();
try {
level = 0;
System.out.println(fact(1 << 15));
} catch (StackOverflowError e) {
System.err.println("true recursion level was " + level);
System.err.println("reported recursion level was "
+ e.getStackTrace().length);
}
}


}

很难给出一个明智的解决方案,因为你热衷于避免所有明智的方法。重构一行代码是明智的解决方案。

注意: 使用-Xss 设置每个线程的堆栈大小,这是一个非常糟糕的主意。

另一种方法是字节码操作,以更改代码如下;

public static long fact(int n) {
return n < 2 ? n : n > 127 ? 0 : n * fact(n - 1);
}

给定 n > 127的每个答案都是0,这样可以避免更改源代码。

还有一些发帖人指出了如何增加内存,以及如何记忆调用。我建议对于许多应用,你可以用斯特林公式来近似大 n!非常快,几乎没有内存占用。

看看这篇文章,其中有一些功能和代码的分析:

Http://threebrothers.org/brendan/blog/stirlings-approximation-formula-clojure/

我做了 字谜游戏,这是类似于 计数变化的问题,但与50000面额(硬币)。我是 不确定是否可以迭代完成,我不在乎。我只知道-xss 选项没有任何效果——我总是在1024个堆栈帧之后失败(可能 scala 不能很好地将数据交付到 java 或 printStackTrace 限制)。我不知道)。不管怎么说,这都是个糟糕的选择。您不希望应用程序中的所有线程都是可怕的。但是,我用新的 Thread (堆栈大小)做了一些实验。这确实管用,

  def measureStackDepth(ss: Long): Long = {
var depth: Long = 0
val thread: Thread = new Thread(null, new Runnable() {
override def run() {
try {
def sum(n: Long): Long = {depth += 1; if (n== 0) 0 else sum(n-1) + 1}
println("fact = " + sum(ss * 10))
} catch {
case e: StackOverflowError => // eat the exception, that is expected
}
}
}, "deep stack for money exchange", ss)
thread.start()
thread.join()
depth
}                                               //> measureStackDepth: (ss: Long)Long




for (ss <- (0 to 10)) println("ss = 10^" +  ss + " allows stack of size " -> measureStackDepth((scala.math.pow (10, ss)).toLong) )
//> fact = 10
//| (ss = 10^0 allows stack of size ,11)
//| fact = 100
//| (ss = 10^1 allows stack of size ,101)
//| fact = 1000
//| (ss = 10^2 allows stack of size ,1001)
//| fact = 10000
//| (ss = 10^3 allows stack of size ,10001)
//| (ss = 10^4 allows stack of size ,1336)
//| (ss = 10^5 allows stack of size ,5456)
//| (ss = 10^6 allows stack of size ,62736)
//| (ss = 10^7 allows stack of size ,623876)
//| (ss = 10^8 allows stack of size ,6247732)
//| (ss = 10^9 allows stack of size ,62498160)

您可以看到,随着分配给线程的堆栈以指数方式增加,堆栈可以指数方式增长。

添加此选项

--driver-java-options -Xss512m

您的火花提交命令将修复这个问题。