如何预测递归方法的最大调用深度?

时间:2012-12-21 19:15:03

标签: java memory recursion jvm stack-overflow

为了估计最大调用深度,递归方法可以用给定的内存量来实现,在堆栈溢出错误可能发生之前计算所用内存的(近似)公式是什么?

编辑:

许多人回答“它取决于”,这是合理的,所以让我们通过使用一个微不足道但具体的例子来删除一些变量:

public static int sumOneToN(int n) {
    return n < 2 ? 1 : n + sumOneToN(n - 1);
}

很容易证明,在我的Eclipse IDE中运行它会导致n低于1000(对我来说非常低)。 这个调用深度限制是否可以在不执行的情况下进行估算?

编辑:我不禁认为Eclipse的固定最大调用深度为1000,因为我得到了998,但是有一个用于main,一个用于初始调用该方法,总共1000。这是一个“太圆”的数字恕我直言,是一个巧合。我会进一步调查。我只是Dux开销-Xss vm参数;它是最大堆栈大小,因此Eclipse运行者必须在某处设置-Xss1000

6 个答案:

答案 0 :(得分:25)

这显然是JVM-也可能是特定于架构的。

我测量了以下内容:

  static int i = 0;
  public static void rec0() {
      i++;
      rec0();
  }

  public static void main(String[] args) {
      ...
      try {
          i = 0; rec0();
      } catch (StackOverflowError e) {
          System.out.println(i);
      }
      ...
  }

使用

Java(TM) SE Runtime Environment (build 1.7.0_09-b05)
Java HotSpot(TM) 64-Bit Server VM (build 23.5-b02, mixed mode)

在x86上运行。

使用20MB的Java堆栈(-Xss20m),摊销成本在每次调用16-17个字节左右波动。我看到的最低值是16.15字节/帧。因此,我得出结论,成本是16字节,其余是其他(固定)开销。

采用单个int的函数具有基本相同的成本,16字节/帧。

有趣的是,一个需要10 ints的函数需要32个字节/帧。我不确定为什么成本太低。

在代码进行JIT编译后,上述结果适用。在编译之前,每帧成本更多,更多更高。我还没有找到一种可靠的估算方法。 但是,这确实意味着您无法可靠地预测最大递归深度,直到您可以可靠地预测递归函数是否已经过JIT编译。

所有这些都经过测试,ulimit堆栈大小为128K和8MB。两种情况下的结果相同。

答案 1 :(得分:10)

只有部分答案:来自JVM Spec 7, 2.5.2,堆栈帧可以在堆上分配,堆栈大小可以是动态的。我无法肯定地说,但似乎应该可以让你的堆栈大小仅限于你的堆大小:

  

因为除了推送和弹出帧之外,永远不会直接操作Java虚拟机堆栈,所以可以对堆进行堆分配。

  

此规范允许Java虚拟机堆栈   固定大小或根据需要动态扩展和收缩   计算。如果Java虚拟机堆栈具有固定大小,   可以选择每个Java虚拟机堆栈的大小   创建该堆栈时独立。

     

Java虚拟机实现可以提供程序员或   用户控制Java虚拟机堆栈的初始大小,   以及在动态扩展或收缩Java的情况下   虚拟机堆栈,控制最大和最小尺寸。

所以这取决于JVM的实现。

答案 2 :(得分:4)

Addig to NPEs回答:

最大堆栈深度似乎灵活。以下测试程序打印非常不同的数字:

public class StackDepthTest {
    static int i = 0;

    public static void main(String[] args) throws Throwable {
        for(int i=0; i<10; ++i){
            testInstance();
        }
    }

    public static void testInstance() {
        StackDepthTest sdt = new StackDepthTest();
        try {
            i=0;
            sdt.instanceCall();
        } catch(StackOverflowError e){}
        System.out.println(i);
    }

    public void instanceCall(){
        ++i;
        instanceCall();
    }
}

输出结果为:

10825
10825
59538
59538
59538
59538
59538
59538
59538
59538

我使用了此JRE的默认值:

java version "1.7.0_09"
OpenJDK Runtime Environment (IcedTea7 2.3.3) (7u9-2.3.3-0ubuntu1~12.04.1)
OpenJDK 64-Bit Server VM (build 23.2-b09, mixed mode)

所以结论是:如果你有足够的脓(即两次以上),你会得到第二次机会; - )

答案 3 :(得分:3)

答案是,这一切都取决于。

首先,可以更改Java堆栈大小。

其次,给定方法的堆栈帧大小可以根据不同的变量而变化。您可以查看Call stack - Wikipedia的“调用堆栈帧大小”部分以获取更多详细信息。

答案 4 :(得分:2)

取决于您的系统架构(32位或64位地址),局部变量的数量和方法的参数。 如果tail-recursive没有开销,那么编译器会将其优化为循环。

你看,没有简单的答案。

答案 5 :(得分:2)

您可以采用经验之路并尝试使用代码和-Xss设置。有关详细信息,请参阅此处:JVM option -Xss - What does it do exactly?