try-finallyブロックはStackOverflowErrorを防ぎます


331

次の2つの方法を見てください。

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

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

実行するbar()と明らかに結果はになりますStackOverflowErrorが、実行されfoo()ません(プログラムが無期限に実行されるように見えるだけです)。何故ですか?


17
正式には、finally句の処理中にスローされたエラーが次のレベルに伝播するため、プログラムは最終的に停止します。しかし、息を止めないでください。実行されるステップの数は(最大スタック深度)に対して約2であり、例外のスローも厳密には安くはありません。
ドナルフェロー

3
bar()しかし、それは「正しい」でしょう。
dan04 '15

6
@ dan04:Javaは、完全なスタックトレースを確保するため、およびリフレクションに関連するもの(おそらくスタックトレースにも関係する)のために、TCO、IIRCを実行しません。
ninjalj 2012

4
興味深いことに、私がこれを(Monoを使用して).Netで試したとき、プログラムはStackOverflowエラーでクラッシュし、最終的には呼び出されませんでした。
Kibbee 2012

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以上になる可能性があるので2倍かかります。1秒あたり10,000,000回の呼び出しを行うことができる場合、これには10 ^ 3003秒以上かかるか、宇宙の古さよりも長くなります。


4
いいですね、スタックをできるだけ小さくしようとしても-Xss、深度は[150-210]になるので、2 ^ nは[47-65]桁の数字になります。そんなに長くは待たない、それは私にとって無限大に十分です。
ninjalj 2012

64
@oldrinbちょうどあなたのために、私は深さを5に増やしました;)
Peter Lawrey '15

4
したがって、foo最終的に終了する日の終わりに、結果はStackOverflowError
arshajii 2012

5
数学に従って、うん。スタックオーバーフローに失敗した最後のスタックオーバーフローからの最後のスタックオーバーフローは、...スタックオーバーフロー= Pで終了します。抵抗できませんでした。
WhozCraig

1
これは実際には、try catchコードでもスタックオーバーフローエラーが発生することを意味しますか?
LPD 2013年

40

あなたがの呼び出しから例外を取得するとfoo()の内部tryには、呼び出しfoo()からfinallyと再び再帰的に開始します。それによって別の例外が発生した場合はfoo()、別のinnerから呼び出すfinally()など、ほとんど無限に続きます。


5
おそらく、スタックに新しいメソッドを呼び出すためのスペースがなくなると、StackOverflowError(SOE)が送信されます。SOEの後にいかにfoo()して最終的呼び出すことができますか?
assylias 2012

4
@assylias:十分なスペースがない場合は、あなたが最新から返されfoo()、呼び出し、および呼び出しfoo()finallyあなたの現在のブロックfoo()の呼び出し。
ninjalj 2012

+1からninjalj。オーバーフロー状態のためにfooを呼び出せなくなると、どこから fooを呼び出せなくなります。これには、最終的に(宇宙の時代)終了する理由であるfinallyブロックからのものが含まれます。
WhozCraig 2012

38

次のコードを実行してみてください:

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

上位レベルまで例外をスローする前に、finallyブロックが実行されることがわかります。(出力:

最後に

スレッド「メイン」の例外java.lang.Exception:TEST!test.main(test.java:6)で

メソッドを終了する直前に最終的に呼び出されるので、これは理にかなっています。ただし、これは、firstを取得StackOverflowErrorすると、それをスローしようとしますが、finallyが最初に実行される必要があるため、foo()再度実行され、別のスタックオーバーフローが発生し、最終的に再び実行されます。これは永久に発生し続けるため、例外が実際に出力されることはありません。

ただし、barメソッドでは、例外が発生するとすぐに、上のレベルまでまっすぐにスローされ、印刷されます。


2
反対票。「永遠に起き続ける」は間違っています。他の回答を参照してください。
jcsahnwaldtはGoFundMonicaを

26

これが最終的に終了するという合理的な証拠を提供するために、次のかなり意味のないコードを提供します。注:Javaは私の言語ではありません。私は、ピーターの答えは、サポートするためにのみこれを差し出す質問に対する正しい答えを。

これは、スタックオーバーフローが発生するために呼び出しが発生しない場合の状況をシミュレートしようとします。難しいことの人々は、それはときに、invokeが起こらないということで把握するために失敗しているように思えることはできませんが起こります。

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

この小さな無意味なgooの山の出力は次のとおりです。実際にキャッチされた例外は、驚くかもしれません。ああ、そして完全に予想される32回のtry-calls(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
それは実際には無限ループではありません、あなたが十分に忍耐強いならば、それは最終的に終了します。でも息を止めません。
ライライアン

4
それは無限だと私は思います。スタックの最大深度に達するたびに、例外がスローされ、スタックがほどかれます。ただし、finallyでFooが再び呼び出され、回復したスタックスペースが再び再利用されます。それは例外を投げて前後に行き、次に再び起こるまでスタックをダウします。永遠に。
Kibbee 2012

また、最初のsystem.out.printlnをtryステートメントに含める必要があります。そうしないと、ループが必要以上に巻き戻されます。停止する可能性があります。
Kibbee 2012

1
@Kibbee引数の問題はfoo、2回目に呼び出されたときに、finallyブロック内ではにないということtryです。そのため、スタックを下に戻してスタックオーバーフローを1回作成しますが、2回目は、2回目の呼び出しで生成されたエラーfooを再深化するのではなく、再スローします。
アマロイ2015

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.