Блок Try-finally предотвращает StackOverflowError

331

Взгляните на следующие два метода:

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

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

Запуск bar()явно приводит к a StackOverflowError, но запуск foo()- нет (кажется, что программа работает бесконечно). Это почему?

arshajii
источник
17
Формально программа в конечном итоге остановится, потому что ошибки, возникшие при обработке finallyпредложения, будут распространяться на следующий уровень. Но не задерживай дыхание; количество предпринятых шагов составит около 2 (максимальная глубина стека), и создание исключений также не совсем дешево.
Донал Феллоуз
3
Это было бы "правильно" bar(), хотя.
Ден04
6
@ dan04: Java не выполняет TCO, IIRC не обеспечивает полные трассировки стека и что-то, связанное с отражением (вероятно, также с трассировками стека).
ниндзя
4
Интересно, что когда я попробовал это в .Net (используя Mono), программа потерпела крах с ошибкой StackOverflow, не вызывая окончательно.
Кибби
10
Это худший фрагмент кода, который я когда-либо видел :)
poitroae

Ответы:

332

Это не вечно. При каждом переполнении стека код перемещается в блок finally. Проблема в том, что это займет очень много времени. Порядок времени O (2 ^ N), где N - максимальная глубина стека.

Представьте себе максимальную глубину 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()

Чтобы проработать каждый уровень в блоке finally, нужно вдвое больше, а глубина стека может составлять 10 000 и более Если вы можете делать 10 000 000 вызовов в секунду, это займет 10 ^ 3003 секунды или дольше, чем возраст вселенной.

Питер Лори
источник
4
Хорошо, даже если я попытаюсь сделать стек как можно меньше с помощью -Xss, я получу глубину [150 - 210], так что 2 ^ n в конечном итоге будет [47 - 65] цифрой. Не буду ждать так долго, это достаточно близко к бесконечности для меня.
ниндзя
64
@oldrinb Только для вас я увеличил глубину до 5.;)
Питер Лоури
4
Итак, в конце дня, когда, fooнаконец, завершится, это приведет к StackOverflowError?
Аршаджи
5
после математики, да последнее переполнение стека от последнего finally, которому не удалось переполнить стек, завершится с ... переполнением стека = P. не смог устоять.
WhozCraig
1
Так значит ли это, что код try catch также должен заканчиваться ошибкой stackoverflow?
LPD
40

Когда вы получите исключение из вызова foo()внутри try, вы звоните foo()с finallyи начать снова рекурсию. Когда это вызывает другое исключение, вы будете звонить foo()из другого внутреннего finally()и т. Д. Почти до бесконечности .

ninjalj
источник
5
Предположительно, StackOverflowError (SOE) отправляется, когда в стеке больше нет места для вызова новых методов. Как можно foo()позвонить из, наконец, после SOE?
assylias
4
@assylias: если места недостаточно, вы вернетесь с последнего foo()вызова и вызовете его foo()в finallyблоке текущего foo()вызова.
ниндзя
+1 к ниндзялю Вы не будете вызывать foo из любого места, если не можете вызвать foo из-за условия переполнения. это включает в себя блок finally, поэтому он, в конце концов, (возраст вселенной) закончится.
WhozCraig
38

Попробуйте запустить следующий код:

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

Вы обнаружите, что блок finally выполняется, прежде чем выбросить исключение на уровень выше него. (Вывод:

в заключение

Исключение в ветке "основной" java.lang. Исключение: ТЕСТ! в test.main (test.java:6)

Это имеет смысл, так как, наконец, вызывается перед выходом из метода. Это означает, однако, что, как только вы получите это первым StackOverflowError, он попытается выбросить его, но, наконец, сначала должен выполняться, так что он запускается foo()снова, что вызывает другое переполнение стека, и, как таковое, запускается, наконец, снова. Это происходит вечно, поэтому исключение никогда не печатается.

Однако в вашем методе bar, как только возникает исключение, оно просто выбрасывается прямо на уровень выше и будет напечатано

Алекс Коулман
источник
2
Downvote. «вечно происходит» - это неправильно. Смотрите другие ответы.
jcsahnwaldt говорит GoFundMonica
26

В попытке представить разумные доказательства того, что это в конечном итоге прекратится, я предлагаю следующий довольно бессмысленный код. Примечание: Java не является моим языком, по всей видимости. Я тух это вверх только поддержать ответ Питера, который правильный ответ на этот вопрос.

Это пытается смоделировать условия того, что происходит, когда вызов НЕ может произойти, потому что это приведет к переполнению стека. Мне кажется, что самое трудное, что люди не могут понять, это то, что призыв не происходит, когда он не может произойти.

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

Результат этой маленькой бессмысленной груды слизи следующий, и фактическое обнаруженное исключение может стать неожиданностью; Да, и 32 попытки вызова (2 ^ 5), что вполне ожидаемо:

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
источник
23

Научитесь отслеживать свою программу:

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

Это вывод, который я вижу:

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

Как вы можете видеть, StackOverFlow создается на нескольких уровнях выше, поэтому вы можете выполнять дополнительные шаги рекурсии, пока не достигнете другого исключения, и так далее. Это бесконечная «петля».

Каролий хорват
источник
11
на самом деле это не бесконечный цикл, если вы достаточно терпеливы, он в конце концов прекратится. Я не буду затаить дыхание для этого все же.
Ли Райан
4
Я бы сказал, что это бесконечно. Каждый раз, когда он достигает максимальной глубины стека, он генерирует исключение и раскручивает стек. Однако в конечном итоге он снова вызывает Foo, заставляя его снова использовать пространство стека, которое он только что восстановил. Он будет перебрасывать исключения, а затем возвращаться в стек до тех пор, пока это не произойдет снова. Навсегда.
Кибби
Кроме того, вы захотите, чтобы первый system.out.println был в операторе try, иначе он размотает цикл дальше, чем должен. возможно, вызывая его остановить.
Кибби
1
@Kibbee Проблема с вашим аргументом в том, что когда он вызывает fooвторой раз, в finallyблоке он больше не находится в try. Таким образом, хотя он будет возвращаться вниз по стеку и создавать дополнительные переполнения стека один раз, во второй раз он просто сбросит ошибку, вызванную вторым вызовом foo, вместо повторного углубления.
Амаллой
0

Программа, кажется, работает вечно; это фактически завершается, но это занимает экспоненциально больше времени, чем больше у вас стекового пространства. Чтобы доказать, что это заканчивается, я написал программу, которая сначала истощает большую часть доступного пространства стека, а затем вызывает fooи, наконец, записывает след того, что произошло:

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

Код:

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

Вы можете попробовать это онлайн! (Некоторые пробеги могут вызывать fooбольше или меньше раз, чем другие)

Витрувий
источник