O bloco Try-Finalmente impede StackOverflowError

331

Dê uma olhada nos dois métodos a seguir:

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

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

A execução bar()resulta claramente em a StackOverflowError, mas a execução foo()não (o programa parece funcionar indefinidamente). Por que é que?

arshajii
fonte
17
Formalmente, o programa será interrompido porque os erros lançados durante o processamento da finallycláusula serão propagados para o próximo nível. Mas não prenda a respiração; o número de etapas executadas será de cerca de 2 à (profundidade máxima da pilha) e o lançamento de exceções também não é exatamente barato.
Donal Fellows
3
Seria "correto" para bar(), no entanto.
dan04
6
@ dan04: Java não executa o TCO, IIRC para garantir o rastreamento completo da pilha e para algo relacionado à reflexão (provavelmente relacionado também ao rastreamento da pilha).
Ninjalj 15/09/12
4
Curiosamente, quando experimentei isso no .Net (usando o Mono), o programa travou com um erro do StackOverflow sem sequer chamar finalmente.
Kibbee
10
Este é o pior pedaço de código que eu já vi :)
poitroae

Respostas:

332

Não dura para sempre. Cada estouro de pilha faz com que o código seja movido para o bloco final. O problema é que levará muito, muito tempo. A ordem do tempo é O (2 ^ N), em que N é a profundidade máxima da pilha.

Imagine a profundidade máxima é 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 trabalhar cada nível no bloco final, demore o dobro do tempo que a profundidade da pilha possa ser 10.000 ou mais. Se você puder fazer 10.000.000 de chamadas por segundo, isso levará 10 ^ 3003 segundos ou mais que a idade do universo.

Peter Lawrey
fonte
4
Bom, mesmo se eu tentar fazer a pilha a menor possível -Xss, eu tenho uma profundidade de [150 - 210], então 2 ^ n acaba sendo um número de [47 - 65] dígitos. Não vou esperar tanto, isso é o suficiente para o infinito para mim.
Ninjalj 15/09/12
64
@oldrinb Só para você, eu aumentei a profundidade a 5.;)
Peter Lawrey
4
Então, no final do dia, quando foofinalmente termina, resultará em um StackOverflowError?
precisa saber é o seguinte
5
seguindo a matemática, sim. o último estouro de pilha do último finalmente que falhou ao estourar a pilha será encerrado com ... stack overflow = P. não pude resistir.
precisa saber é o seguinte
1
Então, isso realmente significa que mesmo que o código de tentativa também deva acabar com o erro stackoverflow?
LPD
40

Quando você receber uma exceção da invocação de foo()dentro do try, você chama foo()de finallye começar a recursão novamente. Quando isso causa outra exceção, você liga foo()de outro interno finally(), e assim por diante quase ad infinitum .

ninjalj
fonte
5
Presumivelmente, um StackOverflowError (SOE) é enviado quando não há mais espaço na pilha para chamar novos métodos. Como pode foo()ser chamado finalmente após uma SOE?
assylias 15/09/12
4
@assylias: se não houver espaço suficiente, você retornará da última foo()chamada e chamará foo()no finallybloco da sua foo()chamada atual .
Ninjalj 15/09/12
+1 a ninjalj. Você não chamará foo de qualquer lugar depois que não puder chamar foo devido à condição de estouro. isso inclui do bloco final, e é por isso que, eventualmente (a idade do universo) termina.
WhozCraig
38

Tente executar o seguinte código:

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

Você descobrirá que o bloco finalmente é executado antes de lançar uma exceção no nível acima dele. (Resultado:

Finalmente

Exceção no encadeamento "main" java.lang.Exception: TEST! em test.main (test.java:6)

Isso faz sentido, como finalmente é chamado logo antes de sair do método. Isso significa, no entanto, que assim que você obtê-lo primeiro StackOverflowError, ele tentará lançá-lo, mas o finalmente deve ser executado primeiro, para que seja executado foo()novamente, o que causa um estouro de outra pilha e, como tal, é executado finalmente novamente. Isso continua acontecendo para sempre, portanto a exceção nunca é realmente impressa.

No seu método de barra, no entanto, assim que a exceção ocorre, ela é lançada diretamente para o nível acima e será impressa

Alex Coleman
fonte
2
Voto negativo. "continua acontecendo para sempre" está errado. Veja outras respostas.
jcsahnwaldt diz GoFundMonica
26

Em um esforço para fornecer evidências razoáveis ​​de que isso acabará eventualmente, ofereço o seguinte código bastante sem sentido. Nota: Java NÃO é minha linguagem, por qualquer trecho da imaginação mais vívida. Eu ofereço isso apenas para apoiar a resposta de Peter, que é a resposta correta para a pergunta.

Isso tenta simular as condições do que acontece quando uma chamada NÃO pode acontecer porque introduziria um estouro de pilha. Parece-me a coisa mais difícil que as pessoas não conseguem entender, pois a invocação não acontece quando não pode acontecer.

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

A saída dessa pequena pilha inútil de gosma é a seguinte, e a exceção real capturada pode ser uma surpresa; Ah, e 32 tentativas (2 ^ 5), o que é totalmente 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
fonte
23

Aprenda a rastrear seu 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 é a saída que vejo:

[...]
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 você pode ver, o StackOverFlow é lançado em algumas camadas acima, portanto, você pode executar etapas de recursão adicionais até encontrar outra exceção, e assim por diante. Este é um "loop" infinito.

Karoly Horvath
fonte
11
na verdade, não é um loop infinito; se você for paciente o suficiente, ele terminará. Eu não vou segurar minha respiração por isso.
Lie Ryan
4
Eu diria que é infinito. Cada vez que atinge a profundidade máxima da pilha, lança uma exceção e desenrola a pilha. No entanto, finalmente chama Foo novamente, fazendo com que reutilize novamente o espaço de pilha que acabou de recuperar. Ele vai e volta lançando exceções e depois retornando Dow the stack até que isso aconteça novamente. Para sempre.
Kibbee
Além disso, você deseja que o primeiro system.out.println esteja na instrução try, caso contrário, ele desenrolará o loop além do que deveria. possivelmente causando a parada.
Kibbee
1
@ Kibbee O problema com seu argumento é que, quando ele chama foopela segunda vez, no finallybloco, não está mais em um try. Portanto, embora ele retorne à pilha e crie mais estouros de pilha uma vez, na segunda vez, apenas retrocederá o erro produzido pela segunda chamada para foo, em vez de aprofundar.
precisa saber é
0

O programa simplesmente parece funcionar para sempre; na verdade, termina, mas leva exponencialmente mais tempo quanto mais espaço de pilha você tiver. Para provar que termina, escrevi um programa que primeiro esgota a maior parte do espaço disponível na pilha, depois chama fooe, finalmente, grava um rastro do que aconteceu:

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

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

Você pode experimentá-lo online! (Algumas execuções podem ser feitas foomais ou menos vezes que outras)

Vitruvius
fonte