【问题标题】:Call Main Recursively递归调用 Main
【发布时间】:2014-05-02 17:30:21
【问题描述】:
public class Demo
{
    static int i=0;
    public static void main(String args[])
    {
        System.out.println("Hello"+(i++));
        main(args);
    }
}

在这个程序中,我使用实例变量调用 main。

它在某些时候可以正常运行,但在某些Hello 打印后它给出StackOverFlow 异常。

所以我输入 int 来查找它被打印了多少次。

我运行这个程序,它在i=4158 之后给出异常。

但我运行它几次它给出了不同值的异常我喜欢 4155,4124,4154 等。

据我所知,StackOverFlow 是由于错误或无条件递归调用而生成的。

我试图弄清楚,但不知道到底发生了什么。

我想知道为什么在4158(或其他值)之后?

它依赖于我的系统还是依赖于我的程序?

【问题讨论】:

  • 没有。请。不要这样做。您没有足够的筹码来执行此操作。
  • 你问为什么每次的数字都不一样?或者为什么会发生这种情况?
  • @Mokoto 好的,我不会 :) 但我只是想知道这背后的确切机制。
  • 好的,不知道为什么每次的数字都不一样。至于它发生的原因,您可以对堆栈是什么以及它是如何填充的进行一些研究。也许从en.wikipedia.org/wiki/Call_stack开始

标签: java recursion


【解决方案1】:

首先,您要隐藏您的 args 变量。在您的字段中定义的args 不会被视为您尝试递归调用mainargs

其次,递归最终会用完,但这取决于您为应用程序分配了多少内存,以及当时内存中还有什么。如果您给它提供 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:以后请把这些信息放到cmets中。谢谢!
  • 查看answer 了解更多信息。
【解决方案2】:

参数和局部变量在堆栈上分配(对象存在于堆上的引用类型和引用该对象的变量)。堆栈通常位于地址空间的上端,当它用完时,它会朝向地址空间的底部(即朝向零)。

您的进程也有一个堆,它位于进程的底部。当你分配内存时,这个堆会增长到地址空间的上端。如您所见,堆有可能与堆栈“碰撞”(有点像技术板!!!)。

堆栈溢出的常见原因是错误的递归调用。通常这是由于您的递归函数没有正确的终止条件而导致的,因此它最终会永远调用自己。但是,使用 gui 编程可以生成间接递归。例如,您的应用程序可能正在处理绘画消息,并且在处理它们时可能会调用一个函数,该函数会导致系统发送另一条绘画消息。在这里你没有明确地调用你自己,但是 OS/VM 已经为你做了。

要处理它们,您需要检查您的代码。如果你有调用自己的函数,那么检查你是否有一个终止条件。如果您在调用该函数时检查了您至少修改了其中一个参数,否则对于可重复调用的函数将没有明显的变化,并且终止条件是无用的。

如果您没有明显的递归函数,请检查您是否正在调用任何间接导致您的函数被调用的库函数(如上面的隐式情况)。

【讨论】:

    【解决方案3】:

    这取决于堆栈大小 -Xss 而不是 Xmx。

    我已经在我的 64 位 jvm 上使用值 -Xss128k -Xss256k -Xss512k 测试了您的示例。

    我得到了 969、2467、5436。

    所以我们可以看到向堆栈添加 128k 会产生 ~ 1500 个新调用,而添加 256k 会产生 ~ 3000 个调用。 这意味着一次调用需要大约 80 字节的堆栈内存。 所以其中8个是对arg的引用,其他的看起来像是一些服务信息来控制流(try catch)或其他的东西。

    【讨论】:

      【解决方案4】:

      堆栈溢出是一种常见的编程错误情况,因为您已经达到了可以在不返回的情况下进行多少次递归调用的限制。这不仅仅影响 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 虚拟机和运行时环境的详细信息知之甚少,无法确定,但我猜这个值每次都会略有不同,因为每次你拥有的可用内存量都略有不同运行程序,由于诸如内存垃圾收集之类的东西。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-10-17
        • 2020-09-09
        • 2012-12-06
        • 2011-07-16
        • 2016-03-21
        相关资源
        最近更新 更多