try-finally块可防止StackOverflowError


331

看一下以下两种方法:

public static void foo() {
    try {
        foo();
    } finally {
        foo();
    }
}

public static void bar() {
    bar();
}

bar()清楚地运行会导致StackOverflowError,但foo()不会运行(该程序似乎无限期地运行)。这是为什么?


17
正式地,该程序最终将停止,因为在处理该finally子句期间引发的错误将传播到下一个级别。但是,不要屏住呼吸。所采取的步骤数将约为最大堆栈深度的2倍,并且抛出异常也不是那么便宜。
Donal Fellows 2012年

3
不过,这对于“是正确的” bar()
dan04'9

6
@ dan04:Java不会执行TCO,IIRC来确保具有完整的堆栈跟踪以及与反射相关的内容(可能也与堆栈跟踪相关)。
ninjalj 2012年

4
有趣的是,当我在.Net上(使用Mono)进行尝试时,该程序由于出现StackOverflow错误而崩溃,而没有最终调用。
Kibbee 2012年

10
这是我见过的最糟糕的代码:)
poitroae 2012年

Answers:


332

它不会永远运行。每次堆栈溢出都会导致代码移至finally块。问题在于这将需要非常,非常长的时间。时间顺序为O(2 ^ N),其中N是最大堆栈深度。

想象最大深度为5

foo() calls
    foo() calls
       foo() calls
           foo() calls
              foo() which fails to call foo()
           finally calls
              foo() which fails to call foo()
       finally
           foo() calls
              foo() which fails to call foo()
           finally calls
              foo() which fails to call foo()
    finally calls
       foo() calls
           foo() calls
              foo() which fails to call foo()
           finally calls
              foo() which fails to call foo()
       finally
           foo() calls
              foo() which fails to call foo()
           finally calls
              foo() which fails to call foo()
finally calls
    foo() calls
       foo() calls
           foo() calls
              foo() which fails to call foo()
           finally calls
              foo() which fails to call foo()
       finally
           foo() calls
              foo() which fails to call foo()
           finally calls
              foo() which fails to call foo()
    finally calls
       foo() calls
           foo() calls
              foo() which fails to call foo()
           finally calls
              foo() which fails to call foo()
       finally
           foo() calls
              foo() which fails to call foo()
           finally calls
              foo() which fails to call foo()

要使每个级别进入“ finally”块,需要两倍的时间,而堆栈深度可能是10,000或更大。如果您每秒可以进行10,000,000次呼叫,那么这将花费10 ^ 3003秒或更长的时间。


4
很好,即使我尝试通过减小堆栈-Xss的深度,也得到[150-210]的深度,所以2 ^ n最终是[47-65]位数字。不用等那么久,对我来说这已经足够接近无限。
ninjalj 2012年

64
@oldrinb为您服务,我将深度增加到5 ;;)
Peter Lawrey

4
因此,在foo最后一次终止的一天结束时,它会导致StackOverflowError
arshajii 2012年

5
按照数学,是的。最后失败的最后一个失败的最后一个堆栈溢出将以...退出堆栈溢出= P。无法抗拒。
WhozCraig 2012年

1
那么,这实际上是否意味着即使尝试捕获代码也应最终导致stackoverflow错误?
LPD

40

当你从调用得到一个异常foo()里面的try,你打电话foo()finally和再次开始递归。当这导致另一个异常时,您foo()将从另一个内部调用finally(),依此类推,几乎是ad infinitum


5
据推测,当堆栈上没有更多空间可调用新方法时,将发送StackOverflowError(SOE)。国有企业之后如何才能foo()最终称呼?
assylias 2012年

4
@assylias:如果空间不足,您将从最新的foo()调用返回,并foo()finally当前foo()调用的块中进行调用。
ninjalj 2012年

+1忍者。一旦由于溢出条件而无法调用foo,就不会从任何地方调用foo。这包括来自finally块的信息,这就是为什么它将最终(宇宙年龄)终止的原因。
WhozCraig 2012年

38

尝试运行以下代码:

    try {
        throw new Exception("TEST!");
    } finally {
        System.out.println("Finally");
    }

您会发现在将Exception抛出到最高级别之前,执行了finally块。(输出:

最后

线程“主”中的异常java.lang.Exception:TEST!在test.main(test.java:6)

这是有道理的,因为在退出该方法之前将最终调用该方法。但是,这意味着,一旦您首先获得它StackOverflowError,它将尝试将其抛出,但是必须首先执行finally,因此它将运行foo()再次,这将导致另一个堆栈溢出,因此最终将再次运行。这一直持续发生,因此永远不会实际打印异常。

但是,在您的bar方法中,一旦发生异常,它将被直接抛出到上面的级别,并将被打印


2
下注。“永远保持前进”是错误的。查看其他答案。
jcsahnwaldt说GoFundMonica

26

为了提供合理的证据证明此方法将最终终止,我提供了以下无意义的代码。注意:在任何最生动的想象中,Java都不是我的语言。我只是为了支持彼得的答案,这是对问题正确答案。

这试图模拟无法执行调用时发生的情况,因为这会导致堆栈溢出。在我看来,最难的事情人们都未能把握,当它的调用不会发生不能发生。

public class Main
{
    public static void main(String[] args)
    {
        try
        {   // invoke foo() with a simulated call depth
            Main.foo(1,5);
        }
        catch(Exception ex)
        {
            System.out.println(ex.toString());
        }
    }

    public static void foo(int n, int limit) throws Exception
    {
        try
        {   // simulate a depth limited call stack
            System.out.println(n + " - Try");
            if (n < limit)
                foo(n+1,limit);
            else
                throw new Exception("StackOverflow@try("+n+")");
        }
        finally
        {
            System.out.println(n + " - Finally");
            if (n < limit)
                foo(n+1,limit);
            else
                throw new Exception("StackOverflow@finally("+n+")");
        }
    }
}

这堆毫无意义的小东西的输出如下,所捕获的实际异常可能会令人惊讶。哦,还有32个try-calls(2 ^ 5),这是完全可以预期的:

1 - Try
2 - Try
3 - Try
4 - Try
5 - Try
5 - Finally
4 - Finally
5 - Try
5 - Finally
3 - Finally
4 - Try
5 - Try
5 - Finally
4 - Finally
5 - Try
5 - Finally
2 - Finally
3 - Try
4 - Try
5 - Try
5 - Finally
4 - Finally
5 - Try
5 - Finally
3 - Finally
4 - Try
5 - Try
5 - Finally
4 - Finally
5 - Try
5 - Finally
1 - Finally
2 - Try
3 - Try
4 - Try
5 - Try
5 - Finally
4 - Finally
5 - Try
5 - Finally
3 - Finally
4 - Try
5 - Try
5 - Finally
4 - Finally
5 - Try
5 - Finally
2 - Finally
3 - Try
4 - Try
5 - Try
5 - Finally
4 - Finally
5 - Try
5 - Finally
3 - Finally
4 - Try
5 - Try
5 - Finally
4 - Finally
5 - Try
5 - Finally
java.lang.Exception: StackOverflow@finally(5)

23

了解如何跟踪程序:

public static void foo(int x) {
    System.out.println("foo " + x);
    try {
        foo(x+1);
    } 
    finally {
        System.out.println("Finally " + x);
        foo(x+1);
    }
}

这是我看到的输出:

[...]
foo 3439
foo 3440
foo 3441
foo 3442
foo 3443
foo 3444
Finally 3443
foo 3444
Finally 3442
foo 3443
foo 3444
Finally 3443
foo 3444
Finally 3441
foo 3442
foo 3443
foo 3444
[...]

如您所见,在上面的某些层上抛出了StackOverFlow,因此您可以执行其他递归步骤,直到遇到另一个异常,依此类推。这是一个无限的“循环”。


11
它实际上不是无限循环,如果您足够耐心,它将最终终止。不过,我不会屏住呼吸。
Lie Ryan

4
我认为这是无限的。每次达到最大堆栈深度时,都会引发异常并展开堆栈。但是,最后它再次调用Foo,导致它再次重用刚恢复的堆栈空间。它将来回抛出异常,然后返回Dow堆栈,直到再次发生。永远。
Kibbee 2012年

另外,您将希望第一个system.out.println位于try语句中,否则它将使循环超出预期的范围。可能导致其停止。
Kibbee 2012年

1
@Kibbee您的参数的问题在于,当它foo第二次在finally块中调用时,它不再位于try。因此,虽然它将退回到堆栈并一次产生更多的堆栈溢出,但是第二次它将只是重新抛出第二次调用所产生的错误foo,而不是重新加深。
amalloy 2015年

0

该程序似乎永远运行。它实际上会终止,但是您拥有更多的堆栈空间会花费更多的时间。为了证明它完成了,我编写了一个程序,该程序首先耗尽了大部分可用的堆栈空间,然后调用foo,最后写了一下发生的情况:

foo 1
  foo 2
    foo 3
    Finally 3
  Finally 2
    foo 3
    Finally 3
Finally 1
  foo 2
    foo 3
    Finally 3
  Finally 2
    foo 3
    Finally 3
Exception in thread "main" java.lang.StackOverflowError
    at Main.foo(Main.java:39)
    at Main.foo(Main.java:45)
    at Main.foo(Main.java:45)
    at Main.foo(Main.java:45)
    at Main.consumeAlmostAllStack(Main.java:26)
    at Main.consumeAlmostAllStack(Main.java:21)
    at Main.consumeAlmostAllStack(Main.java:21)
    ...

编码:

import java.util.Arrays;
import java.util.Collections;
public class Main {
  static int[] orderOfOperations = new int[2048];
  static int operationsCount = 0;
  static StackOverflowError fooKiller;
  static Error wontReachHere = new Error("Won't reach here");
  static RuntimeException done = new RuntimeException();
  public static void main(String[] args) {
    try {
      consumeAlmostAllStack();
    } catch (RuntimeException e) {
      if (e != done) throw wontReachHere;
      printResults();
      throw fooKiller;
    }
    throw wontReachHere;
  }
  public static int consumeAlmostAllStack() {
    try {
      int stackDepthRemaining = consumeAlmostAllStack();
      if (stackDepthRemaining < 9) {
        return stackDepthRemaining + 1;
      } else {
        try {
          foo(1);
          throw wontReachHere;
        } catch (StackOverflowError e) {
          fooKiller = e;
          throw done; //not enough stack space to construct a new exception
        }
      }
    } catch (StackOverflowError e) {
      return 0;
    }
  }
  public static void foo(int depth) {
    //System.out.println("foo " + depth); Not enough stack space to do this...
    orderOfOperations[operationsCount++] = depth;
    try {
      foo(depth + 1);
    } finally {
      //System.out.println("Finally " + depth);
      orderOfOperations[operationsCount++] = -depth;
      foo(depth + 1);
    }
    throw wontReachHere;
  }
  public static String indent(int depth) {
    return String.join("", Collections.nCopies(depth, "  "));
  }
  public static void printResults() {
    Arrays.stream(orderOfOperations, 0, operationsCount).forEach(depth -> {
      if (depth > 0) {
        System.out.println(indent(depth - 1) + "foo " + depth);
      } else {
        System.out.println(indent(-depth - 1) + "Finally " + -depth);
      }
    });
  }
}

您可以在线尝试!(某些跑步次数可能foo比其他次数多或少)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.