簡體   English   中英

Try-finally塊阻止StackOverflowError

[英]Try-finally block prevents StackOverflowError

看看以下兩種方法:

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

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

運行bar()顯然會導致StackOverflowError ,但運行foo()不會(程序似乎無限期運行)。 這是為什么?

它不會永遠運行。 每個堆棧溢出都會導致代碼移動到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秒或更長的時間。

當你從調用異常foo()里面try ,你叫foo()finally並再次開始遞歸。 當它導致另一個異常時,你將從另一個內部finally()調用foo() finally() ,幾乎無限制地調用它。

嘗試運行以下代碼:

    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方法中,一旦發生異常,它就會直接拋到上面的級別,然后打印出來

為了提供合理的證據表明這將最終終止,我提供以下相當無意義的代碼。 注意: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)

學會追蹤你的計划:

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在上面的某些層上拋出,因此您可以執行其他遞歸步驟,直到遇到另一個異常,依此類推。 這是一個無限的“循環”。

該計划似乎只是永遠運行; 它實際上終止了,但是你擁有的堆棧空間越多,所需的時間就越多。 為了證明它完成了,我編寫了一個程序,它首先耗盡了大部分可用的堆棧空間,然后調用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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM