Try-finally-Block verhindert StackOverflowError

Lesezeit: 10 Minuten

Benutzer-Avatar
arshajii

Schauen Sie sich die folgenden zwei Methoden an:

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

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

Betrieb bar() führt eindeutig zu a StackOverflowErrorläuft aber foo() nicht (das Programm scheint einfach endlos zu laufen). Warum ist das so?

  • Formal stoppt das Programm schließlich, weil während der Verarbeitung Fehler ausgelöst werden finally -Klausel wird an die nächsthöhere Ebene weitergegeben. Aber halten Sie nicht den Atem an; Die Anzahl der Schritte beträgt etwa 2 hoch (maximale Stack-Tiefe) und das Werfen von Ausnahmen ist auch nicht gerade billig.

    – Donal Fellows

    15. September 2012 um 16:41 Uhr

  • Es wäre “richtig” für bar()obwohl.

    – dan04

    15. September 2012 um 16:47 Uhr

  • @dan04: Java führt keine TCO, IIRC durch, um sicherzustellen, dass vollständige Stack-Traces vorhanden sind, und für etwas, das mit Reflektion zu tun hat (wahrscheinlich auch mit Stack-Traces).

    – ninjalj

    15. September 2012 um 16:47 Uhr

  • Interessanterweise stürzte das Programm, als ich es auf .Net (mit Mono) ausprobierte, mit einem StackOverflow-Fehler ab, ohne jemals endgültig aufgerufen zu haben.

    – Kibbee

    15. September 2012 um 18:55 Uhr

  • Das ist ungefähr das schlechteste Stück Code, das ich je gesehen habe 🙂

    – Poitroae

    16. September 2012 um 9:49 Uhr

Benutzer-Avatar
Peter Lawrey

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

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

Jede Ebene in den Endgültig-Block einzuarbeiten, 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.

  • Schön, auch wenn ich versuche, den Stack so klein wie möglich zu machen -Xssbekomme ich eine Tiefe von [150 – 210]also ist 2^n am Ende a [47 – 65] Ziffern Zahl. Ich werde nicht so lange warten, das ist nah genug an der Unendlichkeit für mich.

    – ninjalj

    15. September 2012 um 17:05 Uhr


  • @oldrinb Nur für dich habe ich die Tiefe auf 5 erhöht. 😉

    – Peter Lawrey

    15. September 2012 um 17:18 Uhr


  • Also, am Ende des Tages, wenn foo schließlich beendet wird, führt dies zu a StackOverflowError?

    – arshajii

    15. September 2012 um 21:29 Uhr


  • Nach der Mathematik, ja. der letzte Stack-Überlauf vom letzten final, der beim Stack-Überlauf fehlgeschlagen ist, wird beendet mit … Stack-Überlauf = P. konnte nicht widerstehen.

    – WhozCraig

    15. September 2012 um 23:13 Uhr

  • Bedeutet dies also tatsächlich, dass Try-Catch-Code auch in einem Stackoverflow-Fehler enden sollte??

    – LPD

    4. Januar 2013 um 13:33 Uhr

Benutzer-Avatar
ninjalj

Wenn Sie eine Ausnahme vom Aufruf von erhalten foo() innerhalb der trydu rufst foo() aus finally und fange wieder an zu rekursieren. Wenn dies zu einer weiteren Ausnahme führt, rufen Sie an foo() von einem anderen Inneren finally()und so weiter fast Ad infinitum.

  • Vermutlich wird ein StackOverflowError (SOE) gesendet, wenn auf dem Stack kein Platz mehr ist, um neue Methoden aufzurufen. Wie kann foo() endlich abgerufen werden nach ein SOE?

    – Assylias

    15. September 2012 um 16:02 Uhr

  • @assylias: Wenn der Platz nicht ausreicht, kehren Sie spätestens ab zurück foo() Aufruf und aufrufen foo() in dem finally Block deines Stroms foo() Aufruf.

    – ninjalj

    15. September 2012 um 16:10 Uhr

  • +1 zu ninjalj. Sie werden nicht von foo aus anrufen irgendwo Sobald Sie foo aufgrund der Überlaufbedingung nicht aufrufen können. dies schließt den Endgültig-Block ein, weshalb dieser schließlich (Zeitalter des Universums) beendet wird.

    – WhozCraig

    16. September 2012 um 6:00 Uhr

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 auf die darüber liegende Ebene geworfen wird. (Ausgabe:

Endlich

Ausnahme im Thread “main” java.lang.Ausnahme: TEST! bei test.main(test.java:6)

Dies ist sinnvoll, da final direkt vor dem Verlassen der Methode aufgerufen wird. Das bedeutet aber, dass man das erst einmal bekommt StackOverflowErrores wird versuchen, es zu werfen, aber das finally muss zuerst ausgeführt werden, damit es ausgeführt wird foo() wieder, der einen weiteren Stapelüberlauf bekommt und als solcher endlich wieder läuft. Dies geschieht immer wieder, sodass die Ausnahme nie wirklich gedruckt wird.

In Ihrer bar-Methode wird sie jedoch, sobald die Ausnahme auftritt, direkt auf die darüber liegende Ebene geworfen und gedruckt

  • Abstimmen. “Das passiert ewig” ist falsch. Siehe andere Antworten.

    – jcsahnwaldt Wiedereinsetzung von Monica

    26. Juli 2018 um 14:48 Uhr

Benutzer-Avatar
WhozCraig

Um einen angemessenen Beweis dafür zu liefern, dass dies irgendwann beendet wird, biete ich den folgenden ziemlich bedeutungslosen Code an. Hinweis: Java ist NICHT meine Sprache, bei aller lebhaftesten Vorstellungskraft. Ich biete dies nur an, um Peters Antwort zu unterstützen, nämlich das richtige Antwort auf die Frage.

Dies versucht, die Bedingungen zu simulieren, was passiert, wenn ein Aufruf NICHT passieren kann, weil er einen Stapelüberlauf einführen würde. Es scheint mir das Schwierigste, was die Leute nicht begreifen, dass der Aufruf nicht geschieht, wenn er erfolgt kann nicht passieren.

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 Häufchens Gänsehaut ist die folgende, und die tatsächlich abgefangene Ausnahme mag überraschen; Oh, und 32 Try-Calls (2^5), was durchaus erwartet wird:

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)

Benutzer-Avatar
Karl Horvath

Lernen Sie, Ihr Programm zu 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 auf eine andere Ausnahme stoßen, und so weiter. Dies ist eine unendliche “Schleife”.

  • Es ist nicht wirklich eine Endlosschleife, wenn Sie geduldig genug sind, wird es schließlich beendet. Dafür werde ich aber nicht die Luft anhalten.

    – Lüge Ryan

    15. September 2012 um 17:19 Uhr


  • Ich würde behaupten, dass es unendlich ist. Jedes Mal, wenn die maximale Stapeltiefe erreicht wird, wird eine Ausnahme ausgelöst und der Stapel abgewickelt. Am Ende ruft es jedoch erneut Foo auf, was dazu führt, dass es den gerade wiederhergestellten Stapelspeicher erneut verwendet. Es wird hin und her gehen, Ausnahmen auslösen und dann den Stack zurückgehen, bis es wieder passiert. Bis in alle Ewigkeit.

    – Kibbee

    15. September 2012 um 18:24 Uhr


  • Außerdem sollten Sie das erste system.out.println in der try-Anweisung haben, da es sonst die Schleife weiter abwickelt, als es sollte. möglicherweise dazu führen, dass es anhält.

    – Kibbee

    15. September 2012 um 18:38 Uhr

  • @Kibbee Das Problem mit Ihrem Argument ist, dass es anruft foo das zweite Mal, in der finally Block, es ist nicht mehr in einem try. Während es also einmal den Stack hinuntergeht und mehr Stack-Überläufe erzeugt, wird es beim zweiten Mal nur den Fehler erneut auslösen, der durch den zweiten Aufruf von erzeugt wurde foostatt erneut zu vertiefen.

    – Amalloy

    20. August 2015 um 2:48 Uhr

Benutzer-Avatar
Vitruvie

Das Programm scheint nur ewig zu laufen; es wird tatsächlich beendet, aber es dauert exponentiell länger, je mehr Stack-Speicherplatz Sie haben. Um zu beweisen, dass es fertig ist, habe ich ein Programm geschrieben, das zuerst den größten Teil des verfügbaren Stack-Speicherplatzes aufbraucht und dann aufruft foound schreibt schließlich eine Spur dessen, 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);
      }
    });
  }
}

Du kannst Probieren Sie es im Internet aus! (Einige Läufe könnten callen foo öfter oder seltener als andere)

  • Es ist nicht wirklich eine Endlosschleife, wenn Sie geduldig genug sind, wird es schließlich beendet. Dafür werde ich aber nicht die Luft anhalten.

    – Lüge Ryan

    15. September 2012 um 17:19 Uhr


  • Ich würde behaupten, dass es unendlich ist. Jedes Mal, wenn die maximale Stapeltiefe erreicht wird, wird eine Ausnahme ausgelöst und der Stapel abgewickelt. Am Ende ruft es jedoch erneut Foo auf, was dazu führt, dass es den gerade wiederhergestellten Stapelspeicher erneut verwendet. Es wird hin und her gehen, Ausnahmen auslösen und dann den Stack zurückgehen, bis es wieder passiert. Bis in alle Ewigkeit.

    – Kibbee

    15. September 2012 um 18:24 Uhr


  • Außerdem sollten Sie das erste system.out.println in der try-Anweisung haben, da es sonst die Schleife weiter abwickelt, als es sollte. möglicherweise dazu führen, dass es anhält.

    – Kibbee

    15. September 2012 um 18:38 Uhr

  • @Kibbee Das Problem mit Ihrem Argument ist, dass es anruft foo das zweite Mal, in der finally Block, es ist nicht mehr in einem try. Während es also einmal den Stack hinuntergeht und mehr Stack-Überläufe erzeugt, wird es beim zweiten Mal nur den Fehler erneut auslösen, der durch den zweiten Aufruf von erzeugt wurde foostatt erneut zu vertiefen.

    – Amalloy

    20. August 2015 um 2:48 Uhr

1359590cookie-checkTry-finally-Block verhindert StackOverflowError

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy