Try-finally 阻止 ***Error

Posted

技术标签:

【中文标题】Try-finally 阻止 ***Error【英文标题】:Try-finally block prevents ***Error 【发布时间】:2012-09-08 11:03:36 【问题描述】:

看看下面两种方法:

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


public static void bar() 
    bar();


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

【问题讨论】:

正式地,程序最终将停止,因为在处理finally 子句期间抛出的错误将传播到下一个级别。但不要屏住呼吸;所采取的步骤数大约是(最大堆栈深度)的 2 倍,并且抛出异常也不便宜。 不过,bar() 是“正确的”。 @dan04:Java 不做 TCO、IIRC 以确保拥有完整的堆栈跟踪,以及与反射相关的事情(可能也与堆栈跟踪有关)。 有趣的是,当我在 .Net 上尝试这个(使用 Mono)时,程序在没有调用 finally 的情况下因 *** 错误而崩溃。 这是我见过的最糟糕的一段代码:) 【参考方案1】:

当您在 try 中调用 foo() 时遇到异常时,您从 finally 调用 foo() 并再次开始递归。当这导致另一个异常时,您将从另一个内部 finally() 调用 foo(),等等几乎无限

【讨论】:

当堆栈上没有更多空间来调用新方法时,可能会发送 ***Error (SOE)。 foo() 怎么能在最后 一个 SOE 之后被调用? @assylias:如果没有足够的空间,您将从最新的foo() 调用返回,并在当前finally 调用的finally 块中调用foo() +1 到 ninjalj。一旦由于溢出条件而无法调用 foo,您将不会从 anywhere 调用 foo。这包括 finally 块,这就是为什么 this 最终(宇宙年龄)终止。【参考方案2】:

尝试运行以下代码:

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

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

终于

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

这是有道理的,因为 finally 在退出方法之前被调用。然而,这意味着一旦你得到第一个***Error,它会尝试抛出它,但 finally 必须首先执行,所以它再次运行foo(),这会导致另一个堆栈溢出,因此最终会再次运行。这种情况一直在发生,因此从未真正打印出异常。

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

【讨论】:

投反对票。 “永远发生”是错误的。查看其他答案。【参考方案3】:

学习跟踪您的程序:

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
[...]

您可以看到 *** 在上面的某些层被抛出,因此您可以执行额外的递归步骤,直到遇到另一个异常,等等。这是一个无限的“循环”。

【讨论】:

它实际上并不是无限循环,如果你有足够的耐心,它最终会终止。不过,我不会为此屏住呼吸。 我认为它是无限的。每次达到最大堆栈深度时,它都会引发异常并展开堆栈。然而在 finally 中它再次调用 Foo 导致它再次重用它刚刚恢复的堆栈空间。它会来回抛出异常,然后返回堆栈直到再次发生。永远。 另外,您会希望第一个 system.out.println 出现在 try 语句中,否则它将比应有的更进一步展开循环。可能导致它停止。 @Kibbee 你的论点的问题是,当它第二次调用foo 时,在finally 块中,它不再在try 中。因此,虽然它会返回堆栈并创建更多堆栈溢出一次,但第二次它只会重新抛出第二次调用 foo 产生的错误,而不是重新加深。【参考方案4】:

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

【讨论】:

很好,即使我尝试通过-Xss 使堆栈尽可能小,我得到的深度是 [150 - 210],所以 2^n 最终是 [47 - 65 ] 位数。不会等那么久,这对我来说已经足够接近无限了。 @oldrinb 只是为了你,我将深度增加到 5。;) 所以,当foo 最终终止时,它会导致***Error 按照数学计算,是的。最后一个最终堆栈溢出失败的堆栈溢出将退出...堆栈溢出=P。无法抗拒。 那么这实际上是否意味着即使 try catch 代码也应该以 *** 错误告终??【参考方案5】:

为了提供合理的证据证明这将最终终止,我提供了以下相当无意义的代码。注意: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("***@try("+n+")");
        
        finally
        
            System.out.println(n + " - Finally");
            if (n < limit)
                foo(n+1,limit);
            else
                throw new Exception("***@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: ***@finally(5)

【讨论】:

【参考方案6】:

该程序似乎永远运行;它实际上终止了,但是您拥有的堆栈空间越多,它所花费的时间就越多。为了证明它完成了,我写了一个程序,首先耗尽了大部分可用的堆栈空间,然后调用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.***Error
    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 ***Error 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 (***Error e) 
          fooKiller = e;
          throw done; //not enough stack space to construct a new exception
        
      
     catch (***Error 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 的次数可能比其他运行更多或更少)

【讨论】:

以上是关于Try-finally 阻止 ***Error的主要内容,如果未能解决你的问题,请参考以下文章

使用没有“catch”块的“try-finally”块

第9项:尽量使用try-with-resources而不是try-finally(Prefer try-with-resources to try-finally)

Java,try-finally 没有捕获

Java io丑陋的try-finally块

如何将此 try-finally 更改为 try-with-resources?

tryresources优于try-finally