Try-finally block으로 StackOverflowError 방지


331

다음 두 가지 방법을 살펴보십시오.

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

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

실행하면 bar()분명히 결과가 StackOverflowError나오지만 실행 foo()되지 않습니다 (프로그램은 무기한으로 실행되는 것 같습니다). 왜 그런 겁니까?


17
공식적으로, finally절을 처리하는 동안 발생하는 오류 가 다음 레벨로 전파 되기 때문에 프로그램은 결국 중지 됩니다. 그러나 숨을 참지 마십시오. 걸음 수는 약 2에서 최대 스택 깊이까지이며 예외를 던지는 것도 정확하게 싸지는 않습니다.
Donal Fellows

3
그러나 "정확한"것입니다 bar().
dan04

6
@ dan04 : Java는 TCO, IIRC를 사용하여 전체 스택 추적을 유지하고 리플렉션 관련 항목 (아마도 스택 추적과 관련이 있음)을 보장하지 않습니다.
ninjalj

4
흥미롭게도 .Net (Mono 사용)에서 이것을 시도했을 때 프로그램은 마침내 호출하지 않고 StackOverflow 오류와 충돌했습니다.
Kibbee

10
이것은 내가 본 것 중 최악의 코드에 관한 것이다 :)
poitroae

답변:


332

영원히 실행되지 않습니다. 각 스택 오버플로로 인해 코드가 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 초 이상이 걸립니다.


4
을 통해 스택을 가능한 한 작게 만들려고해도 -Xss깊이가 [150-210]이므로 2 ^ n은 [47-65] 자릿수입니다. 그렇게 오래 기다리지 않을 것입니다. 그것은 무한대에 가깝습니다.
ninjalj

64
@oldrinb 단지 당신을 위해, 나는 깊이를 5로 증가시켰다.;)
Peter Lawrey

4
따라서 foo마지막으로 종료 되는 날이 끝나면 StackOverflowError?
arshajii

5
수학을 따른다. 스택 오버플로에 실패한 마지막 스택 오버플로는 마지막으로 종료됩니다 ... stack overflow = P. 저항 할 수 없었다.
WhozCraig

1
따라서 이것은 실제로 catch 코드를 시도해도 stackoverflow 오류가 발생한다는 것을 의미합니까?
LPD

40

당신의 호출에서 예외가 발생하면 foo()내부 try, 당신은 전화 foo()에서 finally다시 재귀 시작합니다. 그로 인해 또 다른 예외가 발생하면 다른 foo()inner에서 호출 finally()하는 등 거의 모든 infinitum 입니다.


5
스택에 새 메서드를 호출 할 공간이 더 이상 없을 때 StackOverflowError (SOE)가 전송 될 수 있습니다. 공기업 이후foo() 최종적으로 어떻게 부름을 받을 수 있습니까?
assylias

4
@assylias : 충분한 공간이 아니라면, 최신에서 반환 foo()호출 및 호출 foo()에서 finally현재의 블록 foo()호출.
ninjalj

ninjalj에 +1 오버 플로우 조건으로 인해 foo를 호출 할 수없는 경우 어디서나 foo를 호출 하지 않습니다. 이것은 finally 블록을 포함하며, 이것이 결국 (우주의 시대) 종결되는 이유입니다.
WhozCraig

38

다음 코드를 실행 해보십시오.

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

예외를 던지기 전에 finally 블록이 실행된다는 것을 알 수 있습니다. (산출:

드디어

스레드 "main"java.lang.Exception의 예외 : 테스트! test.main (test.java:6)에서

마지막으로 메소드를 종료하기 직전에 호출되는 것처럼 이것은 의미가 있습니다. 그러나 일단 일단 StackOverflowError그것을 얻으면 그것을 던지려고하지만 마침내 먼저 먼저 실행해야하므로 foo()다시 실행되어 다른 스택 오버플로가 발생하고 마지막으로 다시 실행됩니다. 이것은 영원히 계속 발생하므로 예외는 실제로 인쇄되지 않습니다.

그러나 바 방법에서 예외가 발생하자마자 위의 수준까지 똑바로 던져 인쇄됩니다.


2
공감. "영원히 계속되고있다"는 잘못이다. 다른 답변을 참조하십시오.
jcsahnwaldt에 따르면 GoFundMonica는

26

이 WILL이 결국 종료 될 것이라는 합리적인 증거를 제공하기 위해 다음과 같은 의미없는 코드를 제공합니다. 참고 : 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)

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는 위의 일부 레이어에서 발생하므로 다른 예외가 발생할 때까지 추가 재귀 단계를 수행 할 수 있습니다. 이것은 무한한 "루프"입니다.


11
실제로 무한 루프가 아닙니다. 인내심이 충분하다면 결국 종료됩니다. 그래도 숨을 참지 않습니다.
Lie Ryan

4
나는 그것이 무한하다고 주장 할 것이다. 최대 스택 깊이에 도달 할 때마다 예외가 발생하고 스택이 풀립니다. 그러나 마지막으로 Foo를 다시 호출하여 방금 복구 한 스택 공간을 다시 사용합니다. 예외가 발생하고 다시 발생할 때까지 다우 스택으로 되돌아갑니다. 영원히.
Kibbee

또한 첫 번째 system.out.println을 try 문에 넣으려면 루프를 풀어야합니다. 작동이 멈출 수 있습니다.
Kibbee

1
@Kibbee 인수의 문제점 foofinally블록 에서 두 번째로 호출 할 때 더 이상 a에 있지 않다는 것입니다 try. 따라서 스택으로 돌아가서 스택 오버플로를 한 번 더 생성하는 동안 두 번째 foo로 다시 심화하는 대신 두 번째 호출에서 발생한 오류 를 다시 발생시킵니다.
amalloy

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

당신은 할 수 있습니다 온라인으로보십시오! (일부 실행은 foo다른 것보다 더 많거나 적은 호출을 할 수 있습니다)

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.