【问题标题】:Stack overflow error handling in finally blockfinally 块中的堆栈溢出错误处理
【发布时间】:2013-07-24 01:17:00
【问题描述】:

我有一个java程序,可以无限次运行。

程序代码:

void asd()
{
    try
    {
        //inside try block
        System.out.println("Inside try !!!");
        asd();
    }
    finally
    {
        //inside finally
        System.out.println("Inside finally !!!");
        asd();
    }
}

OUTPUT :此程序通过不断打印两个系统输出来无限运行。

我的问题:在某些时候,它开始从 try 块中抛出 StackOverflowErrors ,因此它到达 finally 块,我们再次递归调用此函数。但是由于我们已经面临 StackOverflowError,finally 块中的递归函数是如何执行的呢?

JVM 如何处理这种情况?如果我们也得到 OutOfMemoryErrors 会发生同样的行为吗?

【问题讨论】:

    标签: java recursion jvm stack-overflow try-finally


    【解决方案1】:

    问题是您的示例程序是病态的。它不会工作,也不能工作。

    但是当我们已经面对StackOverflowError.时,finally 块中的递归函数是如何执行的?

    正在进行一系列相当复杂的调用。让我们假设堆栈可以容纳 3 帧。 “手动执行”为我们提供了一系列调用/调用堆栈快照,如下所示:

    asd()
    asd() > try
    asd() > try > asd() 
    asd() > try > asd() > try 
    asd() > try > asd() > try > asd() // Stack Overflow!
    asd() > try > asd() > finally
    asd() > try > asd() > finally > asd() // Stack Overflow!
    asd() > finally
    asd() > finally > asd()
    asd() > finally > asd() > try
    asd() > finally > asd() > try > asd() // Stack Overflow!
    asd() > finally > asd() > finally
    asd() > finally > asd() > finally > asd() // Stack Overflow!
    
    END
    

    正如您在深度为 3 的堆栈中看到的那样,我们进行了 7 次调用,其中 4 次因堆栈溢出而失败。如果您对深度为 4 的堆栈执行手动执行,您将获得 15 次调用,5 => 31。模式为 N => 2**N - 1 calls

    在您的情况下,默认堆栈将能够容纳数百甚至数千个递归调用。

    假设 N = 100。2**100 是一个非常大的调用数。它不是无限的,但在程序终止之前你可能已经死了。

    JVM 如何处理这种情况?

    如上。 JVM 没有做任何特别的事情。 “有效的无限循环”行为完全取决于您的程序编写方式。

    如果我们也得到OutOfMemoryErrors 也会发生同样的行为吗?

    呃...这取决于您的程序。但我相信您可以编写一个表现出类似行为模式的示例程序。

    【讨论】:

      【解决方案2】:

      假设程序正在执行asd() 方法并且堆栈空间即将结束。还假设该方法不是“内部尝试”和“最终内部”,而是打印一个计数器,告诉您堆栈的距离:

      void asd(int i){
          try{
              //inside try block
              System.out.print(i);
              System.out.println("t");
              asd(i+1);
          }
          finally{
              //inside finally
              System.out.print(i);
              System.out.println("f");
              asd(i+1);
          }
      }
      

      }

      这就是程序即将用完堆栈空间时所做的事情,此时i 为 9154。

      println("t") 的调用会输出字符,然后继续调用 println() 方法。这会使程序耗尽堆栈空间,因此将执行移至finally 块。打印新行时,对 println 的调用再次耗尽堆栈空间。再次抛出错误,并继续执行当前方法调用上方的激活框架中的finally。这使得程序第二次打印f,因为我们已经从堆栈中弹出了一个激活帧,所以这个调用现在正常完成,并打印出一个新行。到目前为止,程序已经给出了这个输出:

      ...
      9152t
      9153t
      9154t9154f9153f              // notice we jumped to i=1953
      

      现在,该方法再次调用自身,现在从 finally 块开始。堆栈空间的情况和之前一样,所以程序执行如上,只是因为我们在一个finally块中进行i=1953的方法调用,所以程序执行结束在方法调用的finally块中i=1952:

      9154t9154f9152f
      

      i=9152 的 finally 块再次调用 asd,传递 i=9153,并且由于现在有足够的堆栈空间来打印来自 try 块的方法输出的完整行:

      9153t
      

      然后继续调用它自己,在这个调用中会再次耗尽堆栈空间,给出输出:

      9154t9154f9153f
      

      ... 其余的输出可以用类似的方式解释:

      9154t9154f9151f
      9152t
      9153t
      9154t9154f9153f
      9154t9154f9152f
      9153t
      9154t9154f9153f
      9154t9154f9150f
      9151t
      9152t
      9153t
      ...
      

      需要注意的是:

      1. 即使在 StackOverflowError 的情况下也会执行 finally 块。
      2. 遇到StackOverflowError 的程序可能处于不可预知的状态。这里唯一可观察到的效果是println 不输出换行符。在更复杂的程序中,这可能意味着您无法相信程序一直在处理的任何事情的状态,而最安全的做法是完全退出。

      【讨论】:

        【解决方案3】:

        不打算处理错误,即 OutOfMemoryError、StackOverflowError 等。它们使 JVM 处于未定义状态,没有任何保证。此时,您的应用程序必须简单地终止,并且您必须解决导致此问题的问题。

        您的应用程序不应尝试处理错误或在错误发生后运行。 如果此时您正在调用递归函数,那么您就是罪魁祸首。结果无法预测。

        请参阅“11.1.1. 异常的种类”: http://docs.oracle.com/javase/specs/jls/se7/html/jls-11.html

        【讨论】:

          【解决方案4】:

          当您在 finally 块中调用 asd 时,您将得到相同的错误 - 您需要让堆栈上的 asd 帧返回/弹出以解决错误

          【讨论】:

            【解决方案5】:

            但是finally块中的递归函数是如何执行的 因为我们已经面临 StackOverflowError 了?

            如果您尝试处理StackOverflowError,它只会导致进一步的后续 StackOverflowErrors,因为您正在尝试在已经满的堆栈上执行进一步的执行。你不应该试图捕捉,使用这些信息来更好地构建你的代码。这是java中未经检查的异常的要点。如果您不熟悉不同类型的异常...

            检查的异常

            已检查异常是在编译时检查的异常,表示需要处理的条件(通过 try-catch 语句),因为它超出了程序的控制范围。例如,如果您想在给定线程内的任何位置调用Thread.sleep(),您将需要处理潜在的InterruptedException,如果线程被不同的线程中断,则可能发生这种情况。由于发生这种情况的可能性在编译时是已知的,因此需要您作为程序员来处理这种情况。

            未经检查的异常

            java.lang.Throwable 类状态...

            为了在编译时检查异常,Throwable 和 Throwable 的任何子类,但不是两者的子类 RuntimeException 或 Error 被视为已检查的异常。

            由于StackOverflowErrorError 的子类,它不被视为已检查异常,因此是“未检查”。 未经检查的异常通常由于编程错误而出现,例如在您的情况下,无限递归的错误终止。如果您尝试访问空变量的某些成员或数组中的无效索引,则可能会出现其他未经检查的异常。这些异常几乎不可能在编译时检查,并且更常用于向程序员显示其代码中的错误。

            进一步阅读

            Java Exceptions

            Checked vs Unchecked Exceptions

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2010-12-19
              • 1970-01-01
              • 2014-01-26
              • 2019-02-16
              • 2011-09-10
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多