Спробуйте остаточно заблокувати запобігання StackOverflowError


331

Погляньте на наступні два методи:

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

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

Запуск bar()чітко призводить до StackOverflowError, але запуск foo()не робить (програма, здається, працює нескінченно). Чому так?


17
Формально програма врешті-решт зупиниться, оскільки помилки, викинуті під час опрацювання finallyпункту, поширяться на наступний рівень вгору. Але не затримуйте дихання; кількість зроблених кроків буде приблизно 2 (максимальна глибина стека), і викиди винятків теж не зовсім дешеві.
Дональні стипендіати

3
Хоча це було б "правильно" bar().
dan04

6
@ dan04: Java не робить TCO, IIRC для забезпечення повних слідів стека, а також для чогось, пов'язаного з відображенням (можливо, це стосується і слідів стека).
ніндзя

4
Цікаво, що, коли я спробував це на .Net (використовуючи Mono), програма вийшла з ладу з помилкою StackOverflow, не визиваючи нарешті.
Кіббі

10
Це про найгірший фрагмент коду, який я коли-небудь бачив :)
poitroae

Відповіді:


332

Це не працює вічно. Кожне переповнення стека змушує код перейти до остаточного блоку. Проблема в тому, що це пройде дуже-дуже довго. Порядок часу дорівнює 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()

Для роботи кожного рівня в остаточному блоці знадобиться вдвічі більше, ніж глибина стека може становити 10 000 або більше. Якщо ви можете здійснити 10 000 000 дзвінків в секунду, це займе 10 ^ 3003 секунди або більше, ніж вік Всесвіту.


4
Добре, навіть якщо я намагаюся зробити стек якомога меншим за допомогою -Xss, я отримую глибину [150 - 210], тому 2 ^ n в кінцевому підсумку є числом [47 - 65] цифр. Не буду чекати так довго, що досить близько до нескінченності для мене.
ніндзя

64
@oldrinb Тільки для вас, я збільшив глибину до 5.;)
Пітер Лоурі

4
Отже, наприкінці дня, коли fooостаточно припиняється, це призведе до StackOverflowError?
Аршаджій

5
слідуючи математиці, юп. останнє переповнення стека з останнього, нарешті, яке не вдалося переповнювати стек, вийде з ... стека переповнення = P. не втримався.
WhozCraig

1
Так це насправді означає навіть те, що код спробу лову також повинен виявитися помилкою stackoverflow ??
LPD

40

Коли ви отримаєте виключення з виклику foo()всередині try, ви телефонуєте foo()з finallyі почати знову рекурсию. Якщо це спричинить інший виняток, вам зателефонують foo()з іншого внутрішнього finally()і так далі майже безмежно .


5
Імовірно, StackOverflowError (SOE) надсилається, коли в стеку немає більше місця для виклику нових методів. Як можна foo()викликати нарешті після ДП?
assylias

4
@assylias: якщо місця не вистачає, ви повернетесь із останнього foo()виклику та покликуєте foo()в finallyблоці поточного foo()виклику.
ніндзя

+1 до ніндзя. Ви не зателефонуєте foo з будь-якого місця, коли ви не можете зателефонувати foo через стан переповнення. це включає в себе, нарешті, блок, ось чому це з часом (вік Всесвіту) закінчиться.
WhozCraig

38

Спробуйте запустити наступний код:

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

Ви побачите, що остаточний блок виконується перед тим, як викинути Виняток до рівня над ним. (Вихід:

Нарешті

Виняток у потоці "main" java.lang.Exception: TEST! на test.main (test.java:6)

Це має сенс, як нарешті називається прямо перед виходом із методу. Це означає, що, як тільки ви отримаєте це першим StackOverflowError, він спробує кинути його, але, нарешті, він повинен виконати спочатку, тому він запускається foo()знову, що отримує чергове переповнення стека, і як таке працює, нарешті, знову. Це відбувається назавжди, тому виняток ніколи насправді не друкується.

У вашому методі бар, проте, як тільки виняток відбудеться, він буде просто викинутий до рівня вище, і він буде надрукований


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

Вихід цієї маленької безглуздої купи goo є наступним, і фактичний виняток, що потрапив, може стати несподіванкою; О, і 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)

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 знаходився в операторі спробу, інакше він розкрутить цикл далі, ніж слід. можливо, це призведе до зупинки.
Kibbee

1
@Kibbee Проблема з вашим аргументом полягає в тому, що коли він дзвонить fooвдруге, в finallyблоці, він більше не знаходиться в try. Таким чином, хоча він повернеться до стеку і створить більше переповнення стека один раз, вдруге він просто скине помилку, викликану другим викликом foo, замість повторного поглиблення.
amalloy

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більше або менше разів, ніж інші)

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.