Der Try-finally-Block verhindert StackOverflowError

331

Schauen Sie sich die folgenden zwei Methoden an:

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

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

Das Ausführen führt bar()eindeutig zu a StackOverflowError, das Ausführen foo()jedoch nicht (das Programm scheint nur auf unbestimmte Zeit ausgeführt zu werden). Warum ist das so?

arshajii
quelle
17
Formal wird das Programm schließlich gestoppt, da Fehler, die während der Verarbeitung der finallyKlausel ausgelöst werden, auf die nächste Ebene übertragen werden. Aber halten Sie nicht den Atem an. Die Anzahl der Schritte beträgt ungefähr 2 bis (maximale Stapeltiefe) und das Auslösen von Ausnahmen ist auch nicht gerade billig.
Donal Fellows
3
Es wäre jedoch "richtig" bar().
Dan04
6
@ dan04: Java führt keine TCO, IIRC durch, um sicherzustellen, dass vollständige Stapelspuren vorhanden sind, und für etwas, das mit Reflexion zusammenhängt (wahrscheinlich auch mit Stapelspuren).
Ninjalj
4
Interessanterweise stürzte das Programm beim Ausprobieren auf .Net (mit Mono) mit einem StackOverflow-Fehler ab, ohne jemals endgültig aufzurufen.
Kibbee
10
Dies ist ungefähr der schlechteste Code, den ich je gesehen habe :)
Poitroae

Antworten:

332

Es läuft nicht für immer. Jeder Stapelüberlauf bewirkt, dass der Code zum finally-Block verschoben wird. Das Problem ist, dass es sehr, sehr lange dauern wird. Die Zeitreihenfolge ist O (2 ^ N), wobei N die maximale Stapeltiefe ist.

Stellen Sie sich vor, die maximale Tiefe beträgt 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()

Das Einarbeiten jedes Levels in den finally-Block dauert doppelt so lange und die Stapeltiefe kann 10.000 oder mehr betragen. Wenn Sie 10.000.000 Anrufe pro Sekunde tätigen können, dauert dies 10 ^ 3003 Sekunden oder länger als das Alter des Universums.

Peter Lawrey
quelle
4
Schön, auch wenn ich versuche, den Stapel über so klein wie möglich zu machen -Xss, erhalte ich eine Tiefe von [150 - 210], sodass 2 ^ n eine [47 - 65] Ziffernzahl ist. Ich werde nicht so lange warten, das ist nahe genug an der Unendlichkeit für mich.
Ninjalj
64
@oldrinb Nur für dich, ich habe die Tiefe auf 5 erhöht;)
Peter Lawrey
4
Also, am Ende des Tages, wenn es fooendgültig endet, wird es zu einem StackOverflowError?
Arshajii
5
Nach der Mathematik, yup. Der letzte Stapelüberlauf vom letzten, bei dem der Stapelüberlauf fehlgeschlagen ist, wird mit ... Stapelüberlauf = P beendet. konnte nicht widerstehen.
WhozCraig
1
Bedeutet dies also tatsächlich, dass auch der Try-Catch-Code in einem Stackoverflow-Fehler enden sollte?
LPD
40

Wenn Sie eine Ausnahme vom Aufruf von foo()inside the erhalten try, rufen Sie foo()von an finallyund beginnen erneut zu rekursieren. Wenn dies eine weitere Ausnahme verursacht, rufen Sie foo()von einem anderen inneren finally()an und so weiter fast unendlich .

Ninjalj
quelle
5
Vermutlich wird ein StackOverflowError (SOE) gesendet, wenn auf dem Stapel kein Platz mehr zum Aufrufen neuer Methoden vorhanden ist. Wie kann foo()man nach einem SOE endlich aufrufen?
Assylias
4
@assylias: Wenn nicht genügend Speicherplatz vorhanden ist, kehren Sie vom letzten foo()Aufruf zurück und rufen foo()im finallyBlock Ihres aktuellen foo()Aufrufs auf.
Ninjalj
+1 bis Ninjalj. Sie werden foo nicht von irgendwoher aufrufen , wenn Sie foo aufgrund der Überlaufbedingung nicht anrufen können. Dies schließt aus dem finally-Block ein, weshalb dieser schließlich (Alter des Universums) endet.
WhozCraig
38

Versuchen Sie, den folgenden Code auszuführen:

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

Sie werden feststellen, dass der finally-Block ausgeführt wird, bevor eine Ausnahme bis zur darüber liegenden Ebene ausgelöst wird. (Ausgabe:

Schließlich

Ausnahme im Thread "main" java.lang.Exception: TEST! at test.main (test.java:6)

Dies ist sinnvoll, da es schließlich unmittelbar vor dem Beenden der Methode aufgerufen wird. Dies bedeutet jedoch, dass, sobald Sie dies zuerst erhalten StackOverflowError, versucht wird, es zu werfen, aber das endgültige muss zuerst ausgeführt werden, damit es foo()erneut ausgeführt wird, was einen weiteren Stapelüberlauf verursacht, und als solches schließlich erneut ausgeführt wird. Dies geschieht für immer, sodass die Ausnahme nie gedruckt wird.

Bei Ihrer Balkenmethode wird die Ausnahme jedoch, sobald sie auftritt, direkt auf die darüber liegende Ebene geworfen und gedruckt

Alex Coleman
quelle
2
Downvote. "passiert für immer" ist falsch. Siehe andere Antworten.
Jcsahnwaldt sagt GoFundMonica
26

Um vernünftige Beweise dafür zu liefern, dass dies irgendwann enden wird, biete ich den folgenden ziemlich bedeutungslosen Code an. Hinweis: Java ist NICHT meine Sprache. Ich biete dies nur an, um Peters Antwort zu unterstützen, die die richtige Antwort auf die Frage ist.

Dies versucht, die Bedingungen zu simulieren, die auftreten, wenn ein Aufruf NICHT stattfinden kann, da dies zu einem Stapelüberlauf führen würde. Es scheint mir das Schwierigste zu sein, was die Leute nicht verstehen, dass der Aufruf nicht stattfindet, wenn er nicht stattfinden kann .

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

Die Ausgabe dieses kleinen sinnlosen Haufens von Gänsehaut ist die folgende, und die tatsächlich gefangene Ausnahme kann eine Überraschung sein; Oh, und 32 Try-Calls (2 ^ 5), was durchaus zu erwarten ist:

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)
WhozCraig
quelle
23

Erfahren Sie, wie Sie Ihr Programm verfolgen:

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

Dies ist die Ausgabe, die ich sehe:

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

Wie Sie sehen können, wird der StackOverFlow auf einige Ebenen darüber geworfen, sodass Sie zusätzliche Rekursionsschritte ausführen können, bis Sie eine andere Ausnahme treffen, und so weiter. Dies ist eine Endlosschleife.

Karoly Horvath
quelle
11
Es ist eigentlich keine Endlosschleife. Wenn Sie geduldig genug sind, wird sie schließlich beendet. Ich werde dafür aber nicht den Atem anhalten.
Lie Ryan
4
Ich würde davon ausgehen, dass es unendlich ist. Jedes Mal, wenn die maximale Stapeltiefe erreicht wird, wird eine Ausnahme ausgelöst und der Stapel abgewickelt. Im letzten Aufruf wird Foo jedoch erneut aufgerufen, wodurch der gerade wiederhergestellte Stapelspeicher erneut verwendet wird. Es wird hin und her gehen, Ausnahmen auslösen und dann den Stapel zurückdrehen, bis es wieder passiert. Für immer.
Kibbee
Außerdem soll diese erste system.out.println in der try-Anweisung enthalten sein, andernfalls wird die Schleife weiter abgewickelt, als sie sollte. möglicherweise zum Stillstand bringen.
Kibbee
1
@Kibbee Das Problem mit Ihrem Argument ist, dass es foobeim zweiten Aufruf im finallyBlock nicht mehr in a ist try. Während es also wieder den Stapel hinuntergeht und einmal mehr Stapelüberläufe erzeugt, wird beim zweiten Mal nur der Fehler, der durch den zweiten Aufruf von erzeugt wurde foo, erneut ausgelöst , anstatt ihn erneut zu vertiefen.
Amalloy
0

Das Programm scheint nur für immer zu laufen; Es wird tatsächlich beendet, benötigt jedoch exponentiell mehr Zeit, je mehr Stapelspeicher Sie haben. Um zu beweisen, dass es fertig ist, habe ich ein Programm geschrieben, das zuerst den größten Teil des verfügbaren Stapelspeichers erschöpft, dann aufruft foound schließlich eine Spur von dem schreibt, was passiert ist:

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

Der Code:

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

Sie können es online versuchen! (Einige Läufe werden möglicherweise foomehr oder weniger oft aufgerufen als andere.)

Vitruv
quelle