it-swarm.com.de

Der Try-finally-Block verhindert StackOverflowError

Sehen Sie sich die folgenden zwei Methoden an:

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

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

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

326
arshajii

Es läuft nicht für immer. Jeder Stapelüberlauf bewirkt, dass der Code in den finally-Block verschoben wird. 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 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()

Um jede Ebene in den finally-Block zu bearbeiten, ist eine doppelte Zeit erforderlich, 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.

328
Peter Lawrey

Wenn Sie eine Ausnahme vom Aufruf von foo() innerhalb von try erhalten, rufen Sie foo() von finally auf und beginnen erneut mit der Rekursion. Wenn dies zu einer weiteren Ausnahme führt, rufen Sie foo() von einer anderen inneren finally() auf und so weiter fast ad infinitum.

40
ninjalj

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 Exception bis zu der darüber liegenden Ebene ausgelöst wird. (Ausgabe:

Endlich

Ausnahme im Thread "main" Java.lang.Exception: TEST! bei test.main (test.Java:6)

Dies macht Sinn, wie es schließlich kurz vor dem Verlassen der Methode aufgerufen wird. Dies bedeutet jedoch, dass, sobald Sie das erste StackOverflowError erhalten, versucht wird, es zu werfen, aber das letzte muss zuerst ausgeführt werden, so dass es erneut foo() ausführt, was einen weiteren Stapelüberlauf bekommt. und so läuft es endlich wieder. Dies geschieht für immer, sodass die Ausnahme nie gedruckt wird.

Sobald in Ihrer Balkenmethode die Ausnahme auftritt, wird sie direkt auf die darüber liegende Ebene geworfen und gedruckt

38
Alex Coleman

In dem Bemühen, vernünftige Beweise dafür zu liefern, dass dies irgendwann enden wird, biete ich den folgenden ziemlich bedeutungslosen Code an. Anmerkung: Java ist NICHT meine Sprache, wie ich es mir vorgestellt habe. Ich biete dies nur an, um Peters Antwort zu unterstützen, die die richtige Antwort auf die Frage.

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

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("[email protected]("+n+")");
        }
        finally
        {
            System.out.println(n + " - Finally");
            if (n < limit)
                foo(n+1,limit);
            else
                throw new Exception("[email protected]("+n+")");
        }
    }
}

Die Ausgabe dieses kleinen, sinnlosen Stapels ist die folgende, und die tatsächlich gefangene Ausnahme mag überraschen; Ach ja, und 32 try-calls (2 ^ 5), was völlig 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: [email protected](5)
26
WhozCraig

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, wird StackOverFlow in einigen Ebenen darüber ausgelöst, sodass Sie weitere Rekursionsschritte ausführen können, bis Sie eine andere Ausnahme treffen, und so weiter. Dies ist eine Endlosschleife.

23
Karoly Horvath

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 beendet ist, habe ich ein Programm geschrieben, das zuerst den größten Teil des verfügbaren Stapelspeichers verbraucht, dann foo aufruft und schließlich einen Trace darüber 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 versuchen Sie es online! (Einige Läufe rufen foo mehr oder weniger oft auf als andere)

0
Vitruvius