Le bloc Try-finally empêche StackOverflowError

331

Jetez un œil aux deux méthodes suivantes:

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

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

L'exécution bar()entraîne clairement un StackOverflowError, mais foo()pas (le programme semble simplement fonctionner indéfiniment). Pourquoi donc?

arshajii
la source
17
Formellement, le programme s'arrêtera finalement car les erreurs levées lors du traitement de la finallyclause se propageront au niveau supérieur. Mais ne retenez pas votre souffle; le nombre d'étapes prises sera d'environ 2 à la (profondeur de pile maximale) et le lancement d'exceptions n'est pas exactement bon marché non plus.
Donal Fellows
3
Ce serait "correct" pour bar(), cependant.
dan04
6
@ dan04: Java ne fait pas TCO, IIRC pour s'assurer d'avoir des traces de pile complètes et pour quelque chose lié à la réflexion (probablement aussi avec les traces de pile).
ninjalj
4
Curieusement, lorsque j'ai essayé cela sur .Net (en utilisant Mono), le programme s'est écrasé avec une erreur StackOverflow sans jamais appeler finalement.
Kibbee
10
Il s'agit du pire morceau de code que j'ai jamais vu :)
poitroae

Réponses:

332

Cela ne fonctionne pas éternellement. Chaque débordement de pile entraîne le déplacement du code vers le bloc finalement. Le problème est que cela prendra très, très longtemps. L'ordre de temps est O (2 ^ N) où N est la profondeur de pile maximale.

Imaginez que la profondeur maximale est de 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()

Pour travailler chaque niveau dans le bloc enfin, prenez deux fois plus longtemps et la profondeur de la pile peut être de 10 000 ou plus. Si vous pouvez faire 10 000 000 d'appels par seconde, cela prendra 10 ^ 3003 secondes ou plus que l'âge de l'univers.

Peter Lawrey
la source
4
Bien, même si j'essaye de rendre la pile aussi petite que possible via -Xss, j'obtiens une profondeur de [150 - 210], donc 2 ^ n finit par être un nombre de [47 - 65] chiffres. Ne pas attendre aussi longtemps, c'est assez proche de l'infini pour moi.
ninjalj
64
@oldrinb Juste pour vous, j'ai augmenté la profondeur à 5.;)
Peter Lawrey
4
Donc, à la fin de la journée, quand foose termine finalement, cela se traduira par un StackOverflowError?
arshajii
5
en suivant les maths, yup. le dernier débordement de pile du dernier finalement qui n'a pas réussi à déborder de pile se terminera par ... débordement de pile = P. n'a pas pu résister.
WhozCraig
1
Donc, cela signifie-t-il même que le code catch devrait également se terminer par une erreur de stackoverflow ??
LPD
40

Lorsque vous obtenez une exception de l'invocation de foo()inside the try, vous appelez foo()de finallyet recommencez à récurser. Lorsque cela provoque une autre exception, vous appelez foo()d'un autre intérieur finally(), et ainsi de suite presque à l' infini .

ninjalj
la source
5
Vraisemblablement, une StackOverflowError (SOE) est envoyée lorsqu'il n'y a plus d'espace sur la pile pour appeler de nouvelles méthodes. Comment peut- foo()on appeler enfin après un SOE?
assylias
4
@assylias: s'il n'y a pas assez d'espace, vous reviendrez de la dernière foo()invocation et invoquerez foo()dans le finallybloc de votre foo()invocation actuelle .
ninjalj
+1 à ninjalj. Vous n'appelerez pas foo de n'importe où une fois que vous ne pourrez pas appeler foo en raison de la condition de débordement. cela inclut le bloc enfin, c'est pourquoi cela finira éventuellement (âge de l'univers).
WhozCraig
38

Essayez d'exécuter le code suivant:

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

Vous constaterez que le bloc finally s'exécute avant de lancer une exception jusqu'au niveau supérieur. (Production:

finalement

Exception dans le thread "principal" java.lang.Exception: TEST! sur test.main (test.java:6)

Cela a du sens, car finalement est appelé juste avant de quitter la méthode. Cela signifie, cependant, qu'une fois que vous l'obtiendrez en premier StackOverflowError, il essaiera de le lancer, mais que finalement doit s'exécuter en premier, donc il s'exécute à foo()nouveau, ce qui entraîne un autre débordement de pile, et en tant que tel s'exécute enfin à nouveau. Cela se produit pour toujours, donc l'exception n'est jamais réellement imprimée.

Dans votre méthode de barre cependant, dès que l'exception se produit, elle est simplement lancée directement au niveau supérieur et sera imprimée

Alex Coleman
la source
2
Downvote. "continue de se produire pour toujours" est faux. Voir d'autres réponses.
jcsahnwaldt dit GoFundMonica le
26

Dans le but de fournir des preuves raisonnables que ce sera finalement résilié, j'offre le code suivant, plutôt insignifiant. Remarque: Java n'est PAS mon langage, à tout point de vue de l'imagination la plus vive. Je profère de cela uniquement pour appuyer la réponse de Peter, qui est la bonne réponse à la question.

Cela tente de simuler les conditions de ce qui se passe lorsqu'une invocation ne peut PAS se produire car elle introduirait un débordement de pile. Il me semble que la chose la plus difficile que les gens ne parviennent pas à saisir est que l'invocation ne se produit pas lorsqu'elle ne peut pas se produire.

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

La sortie de ce petit tas inutile de goo est la suivante, et l'exception réelle capturée peut surprendre; Oh, et 32 ​​try-calls (2 ^ 5), ce qui est tout à fait attendu:

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
la source
23

Apprenez à retracer votre programme:

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

Voici la sortie que je vois:

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

Comme vous pouvez le voir, StackOverFlow est projeté sur certaines couches ci-dessus, vous pouvez donc effectuer des étapes de récursivité supplémentaires jusqu'à ce que vous rencontriez une autre exception, etc. Il s'agit d'une "boucle" infinie.

Karoly Horvath
la source
11
ce n'est pas en fait une boucle infinie, si vous êtes assez patient, cela finira par se terminer. Je ne retiendrai pas mon souffle pour cela.
Lie Ryan
4
Je dirais que c'est infini. Chaque fois qu'il atteint la profondeur de pile maximale, il lève une exception et déroule la pile. Cependant, dans le dernier, il appelle à nouveau Foo, ce qui le fait réutiliser à nouveau l'espace de pile qu'il vient de récupérer. Il va et vient en jetant des exceptions puis en remontant Dow la pile jusqu'à ce que cela se reproduise. Pour toujours.
Kibbee
En outre, vous souhaiterez que ce premier system.out.println soit dans l'instruction try, sinon il déroulera la boucle plus loin qu'il ne devrait. provoquant éventuellement son arrêt.
Kibbee
1
@Kibbee Le problème avec votre argument est que lorsqu'il appelle foola deuxième fois, dans le finallybloc, il n'est plus dans a try. Ainsi, alors qu'il redescendra la pile et créera plus de débordements de pile une fois, la deuxième fois, il renverra simplement l'erreur produite par le deuxième appel à foo, au lieu de réapprofondir.
amalloy
0

Le programme semble simplement fonctionner pour toujours; il se termine en fait, mais cela prend exponentiellement plus de temps lorsque vous disposez de plus d'espace de pile. Pour prouver qu'il se termine, j'ai écrit un programme qui épuise d'abord la majeure partie de l'espace de pile disponible, puis appelle fooet écrit enfin une trace de ce qui s'est passé:

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

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

Vous pouvez l' essayer en ligne! (Certaines courses peuvent appeler fooplus ou moins de fois que d'autres)

Vitruve
la source