Chặn thử cuối cùng ngăn chặn StackOverflowError


331

Hãy xem hai phương pháp sau:

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

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

Chạy bar()rõ ràng dẫn đến một StackOverflowError, nhưng chạy foo()không (chương trình dường như chạy vô thời hạn). Tại sao vậy?


17
Chính thức, chương trình cuối cùng sẽ dừng lại vì các lỗi được đưa ra trong quá trình xử lý finallymệnh đề sẽ lan truyền lên cấp độ tiếp theo. Nhưng đừng nín thở; số bước được thực hiện sẽ vào khoảng 2 đến (độ sâu ngăn xếp tối đa) và việc ném ngoại lệ cũng không chính xác.
Donal Fellows

3
Nó sẽ là "chính xác" cho bar(), mặc dù.
dan04

6
@ dan04: Java không thực hiện TCO, IIRC để đảm bảo có dấu vết ngăn xếp đầy đủ và đối với một cái gì đó liên quan đến sự phản chiếu (có lẽ cũng phải làm với dấu vết ngăn xếp).
ninjalj

4
Thật thú vị khi tôi đã thử điều này trên .Net (sử dụng Mono), chương trình đã gặp sự cố với lỗi StackOverflow mà không bao giờ gọi cuối cùng.
Kibbee

10
Đây là về đoạn mã tồi tệ nhất tôi từng thấy :)
poitroae

Câu trả lời:


332

Nó không chạy mãi mãi. Mỗi lần tràn ngăn xếp khiến mã di chuyển đến khối cuối cùng. Vấn đề là nó sẽ mất một thời gian rất dài. Thứ tự thời gian là O (2 ^ N) trong đó N là độ sâu ngăn xếp tối đa.

Hãy tưởng tượng độ sâu tối đa là 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()

Để làm việc mỗi cấp vào khối cuối cùng phải mất gấp đôi thời gian độ sâu ngăn xếp có thể là 10.000 hoặc hơn. Nếu bạn có thể thực hiện 10.000.000 cuộc gọi mỗi giây, việc này sẽ mất 10 ^ 3003 giây hoặc lâu hơn tuổi của vũ trụ.


4
Thật tuyệt, ngay cả khi tôi cố gắng làm cho ngăn xếp càng nhỏ càng tốt thông qua -Xss, tôi có được độ sâu [150 - 210], vì vậy 2 ^ n cuối cùng là một số [47 - 65] chữ số. Không phải đợi lâu, điều đó gần đủ đến vô cùng đối với tôi.
ninjalj

64
@oldrinb Chỉ dành cho bạn, tôi đã tăng độ sâu lên 5 .;)
Peter Lawrey

4
Vì vậy, vào cuối ngày khi foocuối cùng chấm dứt, nó sẽ dẫn đến một StackOverflowError?
arshajii

5
theo toán học, yup. tràn ngăn xếp cuối cùng từ cuối cùng cuối cùng đã thất bại trong ngăn xếp tràn sẽ thoát với ... stack overflow = P. không thể cưỡng lại.
WhozCraig

1
Vì vậy, điều này thực sự có nghĩa là ngay cả khi thử mã bắt cũng sẽ gặp lỗi stackoverflow ??
LPD

40

Khi bạn nhận được một ngoại lệ từ lời mời foo() bên trong try, bạn gọi foo()từ finallyvà bắt đầu đệ quy lại. Khi điều đó gây ra một ngoại lệ khác, bạn sẽ gọi foo()từ bên trong khác finally(), và cứ như vậy gần như vô tận quảng cáo .


5
Có lẽ, một StackOverflowError (SOE) được gửi khi không còn chỗ trống trên ngăn xếp để gọi các phương thức mới. Làm thế nào có thể foo()được gọi từ cuối cùng sau một SOE?
assylias

4
@assylias: nếu không có đủ dung lượng, bạn sẽ trở về từ lệnh foo()gọi mới nhất và gọi foo()trong finallykhối lệnh foo()gọi hiện tại của bạn .
ninjalj

+1 đến ninjalj. Bạn sẽ không gọi foo từ bất cứ đâu một khi bạn không thể gọi foo do điều kiện tràn. điều này bao gồm từ khối cuối cùng, đó là lý do tại sao điều này cuối cùng (tuổi của vũ trụ) chấm dứt.
WhozCraig

38

Hãy thử chạy đoạn mã sau:

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

Bạn sẽ thấy rằng khối cuối cùng thực thi trước khi ném Ngoại lệ lên đến mức trên nó. (Đầu ra:

Cuối cùng

Ngoại lệ trong luồng "chính" java.lang.Exception: TEST! tại test.main (test.java:6)

Điều này có ý nghĩa, vì cuối cùng được gọi đúng trước khi thoát khỏi phương thức. Tuy nhiên, điều này có nghĩa là một khi bạn nhận được nó trước StackOverflowError, nó sẽ cố gắng ném nó, nhưng cuối cùng phải thực hiện trước, để nó chạy foo()lại, điều này sẽ khiến một ngăn xếp khác tràn ra, và như vậy cuối cùng lại chạy. Điều này tiếp tục xảy ra mãi mãi, vì vậy ngoại lệ không bao giờ thực sự được in.

Tuy nhiên, trong phương thức thanh của bạn, ngay khi có ngoại lệ xảy ra, nó sẽ được ném thẳng lên mức trên và sẽ được in


2
Downvote. "cứ xảy ra mãi mãi" là sai. Xem câu trả lời khác.
jcsahnwaldt nói GoFundMonica

26

Trong nỗ lực cung cấp bằng chứng hợp lý rằng cuối cùng SILL này sẽ chấm dứt, tôi đưa ra mã khá vô nghĩa sau đây. Lưu ý: Java KHÔNG phải là ngôn ngữ của tôi, bởi bất kỳ sự tưởng tượng sống động nhất. Tôi chỉ dâng này lên để hỗ trợ câu trả lời Phêrô, đó là những câu trả lời đúng cho câu hỏi.

Điều này cố gắng mô phỏng các điều kiện của những gì xảy ra khi một lệnh gọi KHÔNG thể xảy ra bởi vì nó sẽ giới thiệu một tràn ngăn xếp. Dường như với tôi điều khó nhất mà mọi người không nắm bắt được là việc gọi không xảy ra khi không thể xảy ra.

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+")");
        }
    }
}

Đầu ra của đống goo vô nghĩa nhỏ này là như sau, và ngoại lệ thực tế bị bắt có thể gây ngạc nhiên; Ồ, và 32 cuộc gọi thử (2 ^ 5), hoàn toàn được mong đợi:

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)

23

Tìm hiểu để theo dõi chương trình của bạn:

public static void foo(int x) {
    System.out.println("foo " + x);
    try {
        foo(x+1);
    } 
    finally {
        System.out.println("Finally " + x);
        foo(x+1);
    }
}

Đây là đầu ra tôi thấy:

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

Như bạn có thể thấy StackOverFlow được ném ở một số lớp ở trên, vì vậy bạn có thể thực hiện các bước đệ quy bổ sung cho đến khi bạn gặp một ngoại lệ khác, v.v. Đây là một "vòng lặp" vô hạn.


11
Nó không thực sự là vòng lặp vô hạn, nếu bạn đủ kiên nhẫn thì cuối cùng nó sẽ chấm dứt. Tôi sẽ không nín thở vì điều đó.
Lie Ryan

4
Tôi sẽ khẳng định rằng nó là vô hạn. Mỗi lần đạt đến độ sâu ngăn xếp tối đa, nó sẽ ném một ngoại lệ và giải phóng ngăn xếp. Tuy nhiên, cuối cùng nó lại gọi Foo một lần nữa khiến nó sử dụng lại không gian ngăn xếp mà nó vừa phục hồi. Nó sẽ quay đi quay lại ném ngoại lệ và sau đó quay trở lại ngăn xếp cho đến khi nó xảy ra lần nữa. Mãi mãi.
Kibbee

Ngoài ra, bạn sẽ muốn system.out.println đầu tiên nằm trong câu lệnh thử, nếu không nó sẽ làm mất vòng lặp hơn mức cần thiết. có thể khiến nó dừng lại
Kibbee

1
@Kibbee Vấn đề với lập luận của bạn là khi nó gọi foolần thứ hai, trong finallykhối, nó không còn trong a try. Vì vậy, trong khi nó sẽ quay trở lại ngăn xếp và tạo ra nhiều ngăn xếp hơn một lần, thì lần thứ hai nó sẽ chỉ sửa lại lỗi được tạo bởi lệnh gọi thứ hai foo, thay vì đào sâu lại.
amalloy

0

Chương trình dường như chỉ chạy mãi mãi; nó thực sự chấm dứt, nhưng cần nhiều thời gian hơn theo cấp số nhân của không gian ngăn xếp. Để chứng minh rằng nó kết thúc, tôi đã viết một chương trình đầu tiên làm cạn kiệt hầu hết không gian ngăn xếp có sẵn, sau đó gọi foovà cuối cùng viết một dấu vết về những gì đã xảy ra:

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)
    ...

Mật mã:

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);
      }
    });
  }
}

Bạn có thể thử trực tuyến! (Một số lần chạy có thể gọi foonhiều lần hoặc ít hơn những lần khác)

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.