Try-finally块防止StackOverflowError

看看下面两种方法:

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


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

运行bar()显然会导致StackOverflowError,但运行foo()不会(程序似乎无限期地运行)。为什么呢?

12336 次浏览

当你从try中调用foo()得到异常时,你从finally调用foo()并再次开始递归。当这导致另一个异常时,你将从另一个内部finally()调用foo(),以此类推,几乎是无限

试着运行下面的代码:

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

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

最后

java.lang.Exception: TEST! test.main (test.java: 6) < / p >

这是有意义的,因为finally是在退出方法之前调用的。然而,这意味着一旦你获得第一个StackOverflowError,它将尝试抛出它,但finally必须首先执行,因此它再次运行foo(),这将导致另一个堆栈溢出,并最终再次运行。这种情况会一直发生,所以异常实际上不会被打印出来。

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

学习跟踪你的程序:

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在上面的一些层上抛出,因此您可以执行额外的递归步骤,直到遇到另一个异常,等等。这是一个无限“循环”。

它不会永远运行。每次堆栈溢出都会导致代码移动到最后一个块。问题是这需要非常非常长的时间。时间的顺序是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,000个电话,这将需要10^3003秒,甚至比宇宙的年龄还要长。< / p >

为了提供合理的证据证明这种情况最终会终止,我提供了以下相当无意义的代码。注意:无论如何,Java都不是我的语言。我提供这个只是为了支持Peter的答案,这是问题的正确答案。

这尝试模拟当调用不能发生时发生的情况,因为它将引入堆栈溢出。在我看来,人们最难理解的事情是,当不能发生时,调用不会发生。

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调用(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)

程序似乎永远在运行;它实际上会终止,但是你拥有的堆栈空间越大,它所花费的时间就会呈指数级增长。为了证明它已经完成,我编写了一个程序,首先耗尽大部分可用的堆栈空间,然后调用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更多或更少)