Try-finally块阻止StackOverflowError

时间:2012-09-15 15:49:19

标签: java recursion stack-overflow try-finally

看看以下两种方法:

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

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

正在运行bar()会产生StackOverflowError,但运行foo()则不会(程序似乎无限期运行)。 为什么?

6 个答案:

答案 0 :(得分:328)

它不会永远运行。每个堆栈溢出都会导致代码移动到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秒或更长的时间。

答案 1 :(得分:40)

当您从foo()中调用try时收到异常时,请从foo()调用finally并再次开始递归。如果这会导致另一个异常,您将从另一个内部foo()调用finally(),依此类推 ad infinitum

答案 2 :(得分:37)

尝试运行以下代码:

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

你会发现finally块在抛出异常直到它上面的级别之前执行。 (输出:

  

最后

     

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

这是有道理的,因为最终在退出方法之前被调用。但是,这意味着,一旦你得到第一个StackOverflowError,它将尝试抛出它,但是finally必须先执行,因此它再次运行foo(),这会导致另一个堆栈溢出,因此终于再次运行。这种情况将永远发生,因此异常永远不会被打印出来。

然而,在你的bar方法中,一旦发生异常,它就会直接抛到上面的级别,并且会被打印

答案 3 :(得分: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次试听(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)

答案 4 :(得分: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在上面的某些层上抛出,因此您可以执行其他递归步骤,直到您遇到另一个异常,依此类推。这是一个无限的“循环”。

答案 5 :(得分: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);
      }
    });
  }
}

你可以try it online!(某些运行可能会比其他运行更多或更少次地调用foo