递归调用主函数

3
public class Demo
{
    static int i=0;
    public static void main(String args[])
    {
        System.out.println("Hello"+(i++));
        main(args);
    }
}

在这个程序中,我正在使用实例变量调用主函数。
它在某个点上正确运行,但在一些“Hello”打印后会出现“StackOverFlow”异常。
因此,我放置了int来查找它被打印了多少次。
我运行这个程序,它在i = 4158之后给出异常。
但是我运行它几次,它在不同的i值处给出异常,如4155、4124、4154等。
由于不良或无条件递归调用,在我知道这里会生成“StackOverFlow”。
我试图弄清楚它,但不知道到底发生了什么。
我想知道为什么会在4158(或其他值)之后出现此问题?
它是依赖于我的系统还是依赖于我的程序?

1
不要这样。你的堆栈空间远远不足以执行此操作。 - Makoto
你是在问为什么每次数字都不同吗?还是在问为什么会发生这种情况? - Dawood ibn Kareem
@Mokoto 好的,我不会这样做 :) 但是我只是想知道背后的确切机制是什么。 - akash
2
好的,我不知道为什么每次数字都不同。至于为什么会发生这种情况,你可以研究一下堆栈是什么以及它如何填充。也许可以从http://en.wikipedia.org/wiki/Call_stack开始。 - Dawood ibn Kareem
1
可能是重复的问题:为什么这个方法会打印出4? - Jatin
同样相关:[Java中深度递归导致的堆栈溢出?](https://dev59.com/NXRA5IYBdhLWcg3wsgLq#861385)。 - user456814
4个回答

3
堆栈溢出是一种常见的编程错误,它会在您达到递归调用次数极限而没有返回时发生。这不仅影响Java语言。
每当您调用一个函数时,都会创建一个“堆栈帧”,其中包含函数的执行上下文,例如其本地变量。但是,每个堆栈帧都会使用一定数量的内存。如果您已经用完了分配给函数调用的可用内存,或者已经达到某些系统/环境强制的限制(例如运行时限制为10兆字节,即使您有1千兆字节的内存可用),就会发生堆栈溢出。
为了避免无限递归条件,您需要在函数确定应该结束递归的情况下设定终止条件/状态。以下是一个示例,其中终止条件是递归深度达到最大值10,此时函数停止调用自身并最终返回:
public class Demo
{
    String args[] = new String[10];
    static int i = 0;

    public static void main(String args[])
    {
        if (i >= 10) {
            return;
        }

        System.out.println("Hello" + i++);
        main(args);
    }
}

关于为什么在上面的示例中i的值不断变化,i基本上表示递归调用在可用内存耗尽之前所到达的深度。我对Java虚拟机和运行环境的细节了解不够,但我猜测每次值略有不同是因为程序每次运行时可用内存量略有不同,这是由于内存垃圾回收等因素导致的。


3
首先,您正在阴影化您的args变量。在您的字段中定义的args不会被视为您尝试在main中递归调用的相同args
其次,递归最终会耗尽内存,但这取决于您为应用程序分配了多少内存以及此时内存中还有什么。如果您给它2GB(或更多)的空间来处理,递归仍将耗尽 - 但可能在更高的值处运行。
例如,当我使用-Xmx6G运行时,我得到的就是这个:
10791
10796
10789

由于我的操作系统正在运行其他程序,因此数字可能会有所不同。
现在,关于它耗尽的原因:你的调用被放置在一个栈中,这不是内存中的有限空间;它可以(有时会)用完。
每次在Java中调用函数时,它都会进入堆栈。
First time through:
 > main(0)

main()总是被调用,因此它总是在栈的底部。

如果我们再次调用main(),那么它的另一个调用将被放置在堆栈上:

Second time through:
 > main(1)
 > main(0)

对于大多数简单应用程序,只有少量调用(不到100个)被放入调用栈中,它们的生命周期很短,因此它们不会在调用栈上持续很长时间。

然而,由于缺乏称为“基本情况”的东西,您的应用程序与众不同。这是您用来决定停止递归的方法。

例如,以著名的阶乘函数为例,该函数陈述如下:

      { 1          if n = 0
n! = <
      { n * (n-1)! if n > 0

我们有一个基本情况:如果n = 0,那么我们不会继续递归下去。否则,我们就继续前进。
以下是代码示例:
public long factorial(int n) {
    return n == 0 ? 1L : n * factorial(n-1);
}

一旦到达我的基本情况,我就停止添加调用堆栈 - 我实际上开始解决它们。

这是一个 factorial(4) 的示例:

> factorial(4)
  > factorial(3)
    > factorial(2)
      > factorial(1)
        > factorial(0)
        > 1
      > 1 * 1
    > 1 * 1 * 2
  > 1 * 1 * 2 * 3
> 1 * 1 * 2 * 3 * 4

所以,这就是说:如果你要做一个递归函数,确保递归可以结束。否则,你会一直遇到这个问题。

@Aditya:将来请把这样的信息放在评论中。谢谢! - user1131435
请查看此答案以获取更多信息。 - Aditya

1
这取决于堆栈大小-Xss而不是Xmx。我已经在64位jvm上测试了您的示例,使用值-Xss128k、-Xss256k、-Xss512k。结果为969、2467、5436。因此,我们可以看到将128k添加到堆栈会增加约1500个新调用,将256k添加会增加约3000个调用。这意味着一个调用大约占用80字节的堆栈内存。因此,其中8个是对arg的引用,其他看起来像一些服务信息以控制流程(try catch)或其他内容。

0

参数和局部变量在堆栈上分配(对于引用类型,对象存储在堆上,变量引用该对象)。堆栈通常位于地址空间的上端,随着使用它而向地址空间的底部移动(即向零方向)。

您的进程还有一个堆,它位于进程的底部。随着您分配内存,此堆可以向地址空间的上端增长。正如您所看到的,堆与堆栈“碰撞”的可能性很大(有点像构造板块!)。

堆栈溢出的常见原因是错误的递归调用。通常,这是由于您的递归函数没有正确的终止条件,因此它会一直调用自己。但是,在 GUI 编程中,可能会生成间接递归。例如,您的应用程序可能正在处理绘制消息,并在处理它们时调用导致系统发送另一个绘制消息的函数。在这里,您并没有明确地调用自己,但操作系统/虚拟机已经为您完成了。

要处理它们,您需要检查您的代码。如果您有调用自身的函数,则请检查是否有终止条件。如果有,请检查在调用函数时是否至少修改了一个参数,否则对于递归调用的函数将没有可见变化,而终止条件是无用的。

如果您没有明显的递归函数,则请检查是否调用了任何库函数,这些库函数间接地导致调用您的函数(如上述隐式情况)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接