为什么Java中的递归不会导致StackOverflower错误?

为什么Java中的递归不会导致StackOverflower错误?,java,recursion,stack-overflow,Java,Recursion,Stack Overflow,为了在Java中测试StackOverflowerError,我编写了以下代码: package recursion_still_not_working; public class Main { public static void main(String[] args) { // System.out.println(fibonacci(50)); System.out.println("Result: " + factorial(3000));

为了在Java中测试
StackOverflowerError
,我编写了以下代码:

package recursion_still_not_working;

public class Main {
    public static void main(String[] args) {
        // System.out.println(fibonacci(50));
        System.out.println("Result: " + factorial(3000));
    }

    public static long fibonacci(long n) {
        if (n > 1) {
            //System.out.println("calculating with " + (n - 1) + " + " + (n - 2));
            return  fibonacci(n - 1) + fibonacci(n - 2);
        } else {
            return n;
        }
    }

    public static long factorial(long n) {
        if (n > 1) {
            System.out.println("calculating with " + n);
            return n * factorial(n - 1);
        }
        System.out.println("base case reached: " + n);
        return n;
    }
}
但是,只有
阶乘
会导致
堆栈溢出错误
,而
斐波那契
则会执行。我在想,我的JVM可能正在进行一些隐藏的优化,它采用了
fibonacci
的情况,而不是
factorial
的情况

什么可以解释这种行为?需要明确的是:我预计会发生堆栈溢出,但这两种情况中的任何一种都不会发生,这让我感到困惑。我对确实发生的堆栈溢出并不感到惊讶

我的JVM是:

openjdk 11.0.3 2019-04-16
OpenJDK Runtime Environment (build 11.0.3+7-Ubuntu-1ubuntu218.04.1)
OpenJDK 64-Bit Server VM (build 11.0.3+7-Ubuntu-1ubuntu218.04.1, mixed mode, sharing)

当堆栈已满时,会出现堆栈溢出异常。因此,您反复调用函数中的函数来触发这种情况

在fibonacci(50)中,调用并没有得到如此高的调用深度。fibinaci(n)的调用深度仅为n左右。但这需要很长时间,因为每个调用必须执行2个调用,所以最终必须执行2^n个调用。 但是这两个调用是一个接一个地完成的,因此它们不会将这两个调用都添加到堆栈深度中

因此,要进入堆栈溢出异常,您应该: -选择足够高的值作为参数 -设置堆栈的大小


因此,您可以轻松地使用3000作为参数,当您调用它时,可以使用-Xss256k将堆栈大小设置为256K。

如果

 nos_levels * frame_size + overhead > stack_size
在哪里

  • nos_levels
    是问题所需的递归深度
  • frame\u size
    是递归方法的堆栈帧的大小(以字节为单位)
  • 开销
    表示启动递归计算的方法(例如您的
    main
    方法等)的堆栈使用量(以字节为单位)
  • stack\u size
    是堆栈的大小(以字节为单位)
现在您已经实现了阶乘和fibonacii的递归版本。两个版本都将递归到3000的深度,以计算
fibonacci(3000)
factorial(3000)
。两者将使用相同大小的堆栈,并且具有相同的开销

解释为什么一个崩溃而另一个不崩溃的区别是堆栈帧大小

现在,堆栈帧通常包含以下内容:

  • 指向父方法堆栈帧的帧指针
  • 回信地址
  • 该方法的参数
  • 方法声明的局部变量
  • 保存中间值所需的任何未命名临时变量
实际计数将取决于方法的代码,以及JIT编译器如何将变量映射到堆栈帧中的插槽

显然,您的函数差异很大,它们需要大小不同的堆栈。我还没有对此进行验证,但我怀疑是
println
语句在执行此操作。或者更具体地说,当连接字符串参数时,保存中间变量所需的额外隐藏变量


如果您想确定,您需要查看JIT编译器发出的代码。

实际上调用
fibonacci(50)
。如果它没有使您的代码在本地崩溃,请尝试将该值提高到类似于
100
,它最终会死掉。@TimBiegeleisen出于某种原因,它在我的机器上愉快地运行着,但当然不会结束,因为它只会花费太长时间。factorial在运行不到一秒钟的时候就给了我一个堆栈溢出。此外,我没有看到在程序运行时使用越来越多的RAM。您的
factorial(3000)
运行良好,并在不到一秒的时间内给出所有结果。您的斐波那契测试使用的值太小,无法进行比较。对两个测试使用相同的值。此外,您也看不到您的程序使用了更多的RAM,因为堆栈耗尽了内存,而不是更大的堆空间。@forpas哦,不是在我的机器上。现在这个解释有道理了!调用的深度不够,但由于它一分为二,有许多调用需要计算,但它们是按顺序进行的,而不是并行进行的,这就是为什么它们不像在“深度”中那样占用那么多堆栈空间的原因。我已经做了
-Xss256k
的事情,并按照您的描述设置3000个工作项。实际上,这并不能解释阶乘和斐波那契之间的不同行为。具体来说,它没有解释为什么一方给出异常而另一方没有。呼叫计数并不能解释这一点。@StephenC你的眼睛里少了什么?电话号码是重要的一点,我也提到过。因此,要获得堆栈溢出,必须调用另一个函数,直到堆栈满为止,而不返回该函数。这两个函数在堆栈上都需要相同的空间,并且在堆栈内部都有一个对自身的简单调用。当然,一个叫50,另一个叫3000。我是否应该提到,调用长值函数50次所需的空间不如调用3000次所需的空间大?您在回答中对此进行了更详细的解释,但这并不能解释“出于某种原因,它在我的机器上愉快地运行,但当然不会结束”。因此,对我来说,更重要的是解释“呼叫深度”(不返回呼叫的频率)和所需呼叫数之间的差异。虽然这是一个很好的答案,甚至有一些我没有要求的解释,但在我看来,康拉德以最清晰、最相关的方式回答了这个问题。不过,很好的回答,谢谢。