Il blocco Try-finally impedisce StackOverflowError


331

Dai un'occhiata ai seguenti due metodi:

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

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

L'esecuzione bar()chiaramente porta a a StackOverflowError, ma l'esecuzione foo()no (il programma sembra funzionare all'infinito). Perché?


17
Formalmente, il programma alla fine si fermerà perché gli errori generati durante l'elaborazione della finallyclausola si propagheranno al livello successivo. Ma non trattenere il respiro; il numero di passi sarà circa 2 alla (profondità massima dello stack) e anche il lancio di eccezioni non è esattamente economico.
Donal Fellows,

3
Però sarebbe "corretto" bar().
dan04,

6
@ dan04: Java non fa TCO, IIRC per assicurarsi di avere tracce di stack complete e per qualcosa correlato alla riflessione (probabilmente anche con tracce di stack).
ninjalj,

4
È interessante notare che quando ho provato questo su .Net (usando Mono), il programma si è bloccato con un errore StackOverflow senza mai chiamare alla fine.
Kibbee,

10
Si tratta del peggior pezzo di codice che abbia mai visto :)
poitroae,

Risposte:


332

Non corre per sempre. Ogni overflow dello stack fa spostare il codice nel blocco finally. Il problema è che ci vorrà molto, molto tempo. L'ordine del tempo è O (2 ^ N) dove N è la profondità massima dello stack.

Immagina che la profondità massima sia 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()

Per lavorare ogni livello nel blocco finally richiede il doppio della lunghezza della pila che può essere 10.000 o più. Se riesci a effettuare 10.000.000 di chiamate al secondo, occorreranno 10 ^ 3003 secondi o più dell'età dell'universo.


4
Bene, anche se provo a rendere lo stack il più piccolo possibile via -Xss, ottengo una profondità di [150 - 210], quindi 2 ^ n finisce per essere un numero di [47 - 65] cifre. Non aspetterò così tanto, è abbastanza vicino all'infinito per me.
ninjalj,

64
@oldrinb Solo per te, ho aumentato la profondità a 5.;)
Peter Lawrey,

4
Quindi, alla fine della giornata, quando alla foofine termina, si tradurrà in un StackOverflowError?
Arshajii,

5
seguendo la matematica, sì. l'ultimo overflow dello stack dall'ultimo che non è riuscito a impilare lo overflow uscirà con ... stack overflow = P. non ho potuto resistere.
WhozCraig,

1
Quindi questo significa effettivamente che anche provare a catturare il codice dovrebbe finire in errore StackOverflow ??
LPD

40

Quando si riceve un'eccezione dall'invocazione di foo()inside the try, si chiama foo()da finallye si ricomincia da capo. Quando ciò provoca un'altra eccezione, chiamerai foo()da un altro interno finally(), e così via quasi all'infinito .


5
Presumibilmente, viene inviato un StackOverflowError (SOE) quando non c'è più spazio nello stack per chiamare nuovi metodi. Come si può foo()chiamare finalmente da SOE?
Assylias,

4
@assylias: se non c'è abbastanza spazio, tornerai dall'ultima foo()chiamata e invocherai foo()nel finallyblocco della tua attuale foo()chiamata.
ninjalj,

+1 a ninjalj. Non si chiamerà foo da nessuna parte una volta che non è possibile chiamare foo a causa della condizione di overflow. questo include dal blocco finally, motivo per cui questo alla fine (età dell'universo) terminerà.
WhozCraig,

38

Prova a eseguire il seguente codice:

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

Scoprirai che il blocco finally viene eseguito prima di lanciare un'eccezione fino al livello sopra di esso. (Produzione:

Finalmente

Eccezione nel thread "main" java.lang.Exception: TEST! at test.main (test.java:6)

Questo ha senso, come finalmente viene chiamato proprio prima di uscire dal metodo. Ciò significa, tuttavia, che una volta che lo ottieni per primo StackOverflowError, proverà a lanciarlo, ma alla fine deve essere eseguito per primo, quindi viene eseguito di foo()nuovo, il che ottiene un altro overflow dello stack e come tale viene eseguito nuovamente. Questo continua a succedere per sempre, quindi l'eccezione non viene mai realmente stampata.

Nel tuo metodo a barre, tuttavia, non appena si verifica l'eccezione, viene semplicemente lanciato direttamente al livello sopra e verrà stampato


2
Downvote. "continua a succedere per sempre" è sbagliato. Vedi altre risposte
jcsahnwaldt dice GoFundMonica il

26

Nel tentativo di fornire prove ragionevoli che questo alla fine si risolverà, offro il seguente codice piuttosto insignificante. Nota: Java NON è il mio linguaggio, secondo qualsiasi immaginazione più vivida. Lo offro solo per sostenere la risposta di Peter, che è la risposta corretta alla domanda.

Questo tenta di simulare le condizioni di ciò che accade quando un invocazione NON può accadere perché introdurrebbe un overflow dello stack. Mi sembra la cosa più difficile che le persone non riescano a capire in quanto l'invocazione non accade quando non può accadere.

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

L'output di questo piccolo inutile mucchio di sostanza appiccicosa è il seguente, e l'eccezione effettiva catturata può essere una sorpresa; Oh, e 32 try-Calls (2 ^ 5), che è del tutto previsto:

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

Impara a rintracciare il tuo programma:

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

Questo è l'output che vedo:

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

Come puoi vedere, StackOverFlow viene lanciato in alcuni livelli sopra, quindi puoi eseguire ulteriori passaggi di ricorsione fino a quando non raggiungi un'altra eccezione, e così via. Questo è un "loop" infinito.


11
non è in realtà un ciclo infinito, se sei abbastanza paziente finirà per terminare. Non trattengo il respiro per questo però.
Lie Ryan,

4
Suppongo che sia infinito. Ogni volta che raggiunge la profondità massima dello stack genera un'eccezione e scioglie lo stack. Tuttavia, alla fine chiama Foo di nuovo facendolo riutilizzare nuovamente lo spazio dello stack che ha appena recuperato. Andrà avanti e indietro lanciando eccezioni e poi indietro Dow la pila fino a quando non si ripete. Per sempre.
Kibbee,

Inoltre, vorrai che il primo system.out.println sia nell'istruzione try, altrimenti svolgerà il ciclo più lontano di quanto dovrebbe. forse causando l'arresto.
Kibbee,

1
@Kibbee Il problema con il tuo argomento è che quando chiama foola seconda volta, nel finallyblocco, non è più in a try. Quindi, mentre tornerà indietro nello stack e creerà più overflow dello stack una volta, la seconda volta riproverà l'errore prodotto dalla seconda chiamata afoo , invece di approfondire.
amalloy,

0

Il programma sembra funzionare per sempre; in realtà termina, ma richiede in modo esponenziale più tempo più spazio nello stack che hai. Per provare che finisce, ho scritto un programma che prima esaurisce la maggior parte dello spazio disponibile sullo stack, quindi chiama fooe infine scrive una traccia di ciò che è accaduto:

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

Il codice:

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

Puoi provarlo online! (Alcune corse potrebbero chiamare foopiù o meno volte di altre)

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.