El bloque Try-finally evita StackOverflowError

331

Eche un vistazo a los siguientes dos métodos:

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

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

Ejecutar bar()claramente da como resultado a StackOverflowError, pero foo()no lo hace (el programa parece ejecutarse indefinidamente). ¿Porqué es eso?

arshajii
fuente
17
Formalmente, el programa finalmente se detendrá porque los errores lanzados durante el procesamiento de la finallycláusula se propagarán al siguiente nivel. Pero no aguantes la respiración; el número de pasos dados será de aproximadamente 2 a la (profundidad máxima de la pila) y el lanzamiento de excepciones tampoco es exactamente barato.
Donal Fellows
3
Sin embargo, sería "correcto" bar().
dan04
66
@ dan04: Java no hace TCO, IIRC para asegurarse de tener rastros completos de la pila, y para algo relacionado con la reflexión (probablemente también tenga que ver con los rastros de la pila).
ninjalj
44
Curiosamente, cuando probé esto en .Net (usando Mono), el programa se bloqueó con un error de StackOverflow sin siquiera llamar finalmente.
Kibbee
10
Este es el peor fragmento de código que he visto :)
poitroae

Respuestas:

332

No funciona para siempre. Cada desbordamiento de pila hace que el código se mueva al bloque finalmente. El problema es que tomará mucho, mucho tiempo. El orden de tiempo es O (2 ^ N) donde N es la profundidad máxima de la pila.

Imagina que la profundidad máxima es 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()

Para trabajar cada nivel en el bloque finalmente, tome el doble de tiempo y la profundidad de la pila podría ser de 10,000 o más. Si puede hacer 10,000,000 de llamadas por segundo, esto tomará 10 ^ 3003 segundos o más que la edad del universo.

Peter Lawrey
fuente
44
Agradable, incluso si trato de hacer la pila lo más pequeña posible -Xss, obtengo una profundidad de [150 - 210], por lo que 2 ^ n termina siendo un número de dígitos [47 - 65]. No voy a esperar tanto, eso es lo suficientemente cercano al infinito para mí.
ninjalj
64
@oldrinb Solo para ti, aumenté la profundidad a 5.;)
Peter Lawrey
44
Entonces, al final del día, cuando foofinalmente termina, ¿dará como resultado un StackOverflowError?
arshajii
55
siguiendo las matemáticas, sí. el último desbordamiento de la pila desde el último finalmente que no pudo apilar el desbordamiento saldrá con ... desbordamiento de pila = P. No pude resistirme.
WhozCraig
1
Entonces, ¿esto significa que incluso el código de prueba también debería terminar en un error de stackoverflow?
LPD
40

Cuando obtiene una excepción de la invocación de foo()dentro de try, llama foo()desde finallyy comienza a recurrir nuevamente. Cuando eso causa otra excepción, llamarás foo()desde otro interior finally(), y así sucesivamente casi hasta el infinito .

ninjalj
fuente
55
Presumiblemente, se envía un StackOverflowError (SOE) cuando no hay más espacio en la pila para llamar a nuevos métodos. ¿Cómo se foo()puede llamar finalmente después de una SOE?
assylias
44
@assylias: si no hay suficiente espacio, volverá de la última foo()invocación e invocará foo()en el finallybloque de su foo()invocación actual .
ninjalj
+1 a ninjalj. No podrá llamar a foo desde ningún lugar una vez que no pueda llamar a foo debido a la condición de desbordamiento. Esto incluye desde el último bloque, razón por la cual esto, eventualmente (edad del universo) terminará.
WhozCraig
38

Intenta ejecutar el siguiente código:

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

Encontrará que el bloque finalmente se ejecuta antes de lanzar una Excepción hasta el nivel superior. (Salida:

Finalmente

Excepción en el hilo "main" java.lang.Exception: TEST! en test.main (test.java:6)

Esto tiene sentido, ya que finalmente se llama justo antes de salir del método. Esto significa, sin embargo, que una vez que obtenga eso primero StackOverflowError, intentará lanzarlo, pero finalmente debe ejecutarse primero, por lo que se ejecuta foo()nuevamente, lo que genera otro desbordamiento de la pila y, como tal, finalmente se ejecuta nuevamente. Esto sigue sucediendo para siempre, por lo que la excepción nunca se imprime.

Sin embargo, en su método de barra, tan pronto como se produce la excepción, se lanza directamente al nivel superior y se imprimirá

Alex Coleman
fuente
2
Voto a favor. "sigue sucediendo para siempre" está mal. Ver otras respuestas.
jcsahnwaldt dice GoFundMonica
26

En un esfuerzo por proporcionar evidencia razonable de que esto eventualmente terminará, ofrezco el siguiente código bastante sin sentido. Nota: Java NO es mi lenguaje, por cualquier tramo de la imaginación más vívida. Lo propongo solo para apoyar la respuesta de Peter, que es la respuesta correcta a la pregunta.

Esto intenta simular las condiciones de lo que sucede cuando una invocación NO puede suceder porque introduciría un desbordamiento de pila. Me parece que lo más difícil que la gente no comprende es que la invocación no ocurre cuando no puede suceder.

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 salida de este pequeño montón de cosas sin sentido es la siguiente, y la excepción real atrapada puede ser una sorpresa; Ah, y 32 llamadas de prueba (2 ^ 5), lo cual es completamente esperado:

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
fuente
23

Aprenda a rastrear su programa:

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

Esta es la salida que veo:

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

Como puede ver, el StackOverFlow se lanza en algunas capas arriba, por lo que puede realizar pasos de recursión adicionales hasta que encuentre otra excepción, y así sucesivamente. Este es un "bucle" infinito.

Karoly Horvath
fuente
11
en realidad no es un bucle infinito, si eres lo suficientemente paciente eventualmente terminará. Sin embargo, no aguantaré la respiración.
Lie Ryan
44
Yo diría que es infinito. Cada vez que alcanza la profundidad máxima de la pila, lanza una excepción y desenrolla la pila. Sin embargo, finalmente llama a Foo nuevamente y hace que vuelva a usar el espacio de la pila que acaba de recuperar. Irá de un lado a otro lanzando excepciones y luego regresará Dow la pila hasta que vuelva a suceder. Siempre.
Kibbee
Además, querrá que ese primer system.out.println esté en la instrucción try, de lo contrario, se desenrollará el bucle más de lo que debería. posiblemente haciendo que se detenga.
Kibbee
1
@Kibbee El problema con su argumento es que cuando llama por foosegunda vez, en el finallybloque, ya no está en un try. Entonces, si bien volverá a bajar por la pila y creará más desbordamientos de pila una vez, la segunda vez simplemente volverá a lanzar el error producido por la segunda llamada a foo, en lugar de volver a profundizar.
amalloy
0

El programa simplemente parece ejecutarse para siempre; en realidad termina, pero lleva exponencialmente más tiempo cuanto más espacio de pila tenga. Para demostrar que termina, escribí un programa que primero agota la mayor parte del espacio de pila disponible, luego llama fooy finalmente escribe un rastro de lo que sucedió:

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

El código:

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

¡Puedes probarlo en línea! (Algunas ejecuciones pueden llamar foomás o menos veces que otras)

Vitruvio
fuente