335

看看以下两种方法:

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

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

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

4

6 回答 6

333

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

于 2012-09-15T16:25:23.507 回答
40

foo()当您从内部的调用中获得异常时try,您将调用foo()fromfinally并再次开始递归。当这导致另一个异常时,您foo()将从另一个 inner调用finally(),依此类推,几乎无穷无尽

于 2012-09-15T15:53:18.617 回答
38

尝试运行以下代码:

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

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

最后

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

这是有道理的,因为 finally 在退出方法之前被调用。然而,这意味着一旦你得到了 first StackOverflowError,它会尝试抛出它,但 finally 必须首先执行,所以它foo()再次运行,这会导致另一个堆栈溢出,因此 finally 再次运行。这会永远发生,因此实际上从未打印出异常。

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

于 2012-09-15T15:54:39.177 回答
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)
于 2012-09-16T05:50:24.290 回答
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 在上面的某些层被抛出,因此您可以执行额外的递归步骤,直到遇到另一个异常,依此类推。这是一个无限的“循环”。

于 2012-09-15T16:07:30.867 回答
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);
      }
    });
  }
}

You can try it online! (Some runs might call foo more or fewer times than others)

于 2018-04-11T00:08:28.113 回答