Blok „spróbuj nareszcie” zapobiega StackOverflowError

331

Spójrz na następujące dwie metody:

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

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

Uruchamianie bar()wyraźnie powoduje StackOverflowError, ale foo()nie działa (program wydaje się działać bez końca). Dlaczego?

arshajii
źródło
17
Formalnie program ostatecznie się zatrzyma, ponieważ błędy zgłoszone podczas przetwarzania finallyklauzuli zostaną przeniesione na wyższy poziom. Ale nie wstrzymuj oddechu; liczba wykonanych kroków wyniesie około 2 do (maksymalnej głębokości stosu), a rzucanie wyjątków nie jest też do końca tanie.
Donal Fellows,
3
Byłoby to jednak „poprawne” bar().
dan04
6
@ dan04: Java nie wykonuje TCO, IIRC, aby zapewnić posiadanie pełnych śladów stosu i dla czegoś związanego z odbiciem (prawdopodobnie związanym również ze śladami stosu).
ninjalj
4
Co ciekawe, kiedy wypróbowałem to na .Net (przy użyciu Mono), program zawiesił się z błędem StackOverflow, nigdy nie wywołując w końcu.
Kibbee
10
To jest najgorszy fragment kodu, jaki kiedykolwiek widziałem :)
poitroae,

Odpowiedzi:

332

To nie trwa wiecznie. Każde przepełnienie stosu powoduje przejście kodu do bloku ostatecznie. Problem polega na tym, że zajmie to naprawdę bardzo długo. Kolejność czasu wynosi O (2 ^ N), gdzie N jest maksymalną głębokością stosu.

Wyobraź sobie, że maksymalna głębokość wynosi 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()

Aby przepracować każdy poziom w bloku ostatecznie, potrzeba dwa razy więcej, a głębokość stosu może wynosić 10 000 lub więcej. Jeśli wykonasz 10 000 000 połączeń na sekundę, zajmie to 10 ^ 3003 sekund lub więcej niż wiek wszechświata.

Peter Lawrey
źródło
4
Fajnie, nawet jeśli spróbuję zminimalizować stos za pomocą -Xss, otrzymuję głębokość [150 - 210], więc 2 ^ n kończy się na liczbie [47 - 65] cyfr. Nie będę czekać tak długo, to dla mnie wystarczająco blisko nieskończoności.
ninjalj
64
@oldrinb Tylko dla ciebie zwiększyłem głębokość do 5.;)
Peter Lawrey
4
Tak więc pod koniec dnia, kiedy w fookońcu zakończy się, spowoduje to StackOverflowError?
arshajii
5
zgodnie z matematyką, tak. ostatni przepełnienie stosu od ostatniego, który ostatecznie nie przepełnił stosu, zakończy działanie z ... przepełnieniem stosu = P. nie mogłem się oprzeć.
WhozCraig
1
Czy to oznacza, że ​​nawet kod catch catch powinien skończyć się błędem stackoverflow?
LPD
40

Kiedy pojawi się wyjątek z wezwaniem foo()wewnątrz try, zadzwonić foo()z finallyi rozpocząć recursing ponownie. Gdy spowoduje to kolejny wyjątek, zadzwonisz foo()z innego wewnętrznego finally()i tak dalej, prawie bez końca .

ninjalj
źródło
5
Prawdopodobnie StackOverflowError (SOE) jest wysyłany, gdy na stosie nie ma już miejsca na wywołanie nowych metod. Jak można foo()wywołać w końcu po SOE?
assylias
4
@assylias: jeśli nie ma wystarczającej ilości miejsca, wrócisz od ostatniego foo()wywołania i wywołasz foo()w finallybloku bieżącego foo()wywołania.
ninjalj
+1 do ninjalj. Nie będziesz dzwonić do foo z dowolnego miejsca, gdy nie będziesz mógł zadzwonić do foo z powodu przepełnienia. obejmuje to od bloku w końcu i dlatego ostatecznie (wiek wszechświata) zakończy się.
WhozCraig,
38

Spróbuj uruchomić następujący kod:

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

Przekonasz się, że blok w końcu wykonuje się przed rzuceniem wyjątku na poziom powyżej niego. (Wynik:

Wreszcie

Wyjątek w wątku „main” java.lang.Exception: TEST! at test.main (test.java:6)

Ma to sens, ponieważ w końcu jest wywoływane tuż przed wyjściem z metody. Oznacza to jednak, że gdy zdobędziesz go pierwszy StackOverflowError, spróbuje go wyrzucić, ale w końcu musi wykonać najpierw, więc działafoo() ponownie, co powoduje przepełnienie stosu i jako taki działa w końcu ponownie. Dzieje się tak wiecznie, więc wyjątek nigdy nie jest drukowany.

Jednak w metodzie słupkowej, gdy tylko wystąpi wyjątek, jest on po prostu rzucany prosto do poziomu powyżej i zostanie wydrukowany

Alex Coleman
źródło
2
Głosuj „dzieje się wiecznie” jest błędne. Zobacz inne odpowiedzi.
jcsahnwaldt mówi GoFundMonica
26

W celu dostarczenia uzasadnionego dowodu, że ta WOLA ostatecznie wygasa, oferuję następujący raczej bezsensowny kod. Uwaga: Java nie jest moim językiem, pod żadnym względem najbardziej żywej wyobraźni. I propozycja to tylko do wspierania odpowiedź Piotra, która jest poprawną odpowiedź na pytanie.

Ta próba symulacji warunków tego, co się dzieje, gdy wywołanie NIE może się zdarzyć, ponieważ spowodowałoby to przepełnienie stosu. Wydaje mi się, że najtrudniejszą rzeczą, której ludzie nie rozumieją, jest fakt, że inwokacja nie zdarza się, gdy nie może .

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

Rezultat tego małego bezsensownego stosu maź jest następujący, a faktyczny wyjątek może być zaskoczeniem; Aha, i 32 try-call (2 ^ 5), co jest całkowicie oczekiwane:

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
źródło
23

Naucz się śledzić swój program:

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

Oto wynik, który widzę:

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

Jak widać StackOverFlow jest rzucany na niektóre warstwy powyżej, więc możesz wykonać dodatkowe kroki rekurencyjne, aż trafisz na inny wyjątek i tak dalej. To jest nieskończona „pętla”.

Karoly Horvath
źródło
11
w rzeczywistości nie jest to nieskończona pętla, jeśli będziesz wystarczająco cierpliwy, ostatecznie się skończy. Nie wstrzymam jednak oddechu.
Lie Ryan,
4
Sądzę, że jest nieskończony. Za każdym razem, gdy osiąga maksymalną głębokość stosu, rzuca wyjątek i rozwija stos. Jednak w końcu wywołuje ponownie Foo, co powoduje, że ponownie wykorzystuje odzyskane miejsce na stosie. Będzie się poruszać w tę iz powrotem, wyrzucając wyjątki, a następnie wracając Dow stosu, aż to się powtórzy. Na zawsze.
Kibbee
Będziesz także chciał, aby pierwszy system.out.println znalazł się w instrukcji try, w przeciwnym razie rozwinie on pętlę dalej niż powinien. prawdopodobnie powodując zatrzymanie.
Kibbee
1
@Kibbee Problem z twoim argumentem polega na tym, że gdy wywołuje foosię drugi raz, w finallybloku, nie jest już w try. Więc chociaż wróci on do stosu i spowoduje więcej przepełnień stosu, po raz drugi po prostu wyrzuci błąd spowodowany przez drugie wywołanie foo, zamiast ponownego pogłębiania.
amalloy
0

Program wydaje się działać wiecznie; faktycznie kończy się, ale zajmuje więcej czasu, im więcej masz miejsca na stosie. Aby udowodnić, że się kończy, napisałem program, który najpierw wyczerpuje większość dostępnego miejsca na stosie, a następnie wywołuje foo, a na koniec zapisuje ślad tego, co się stało:

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

Kod:

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

Możesz spróbować online! (Niektóre biegi mogą dzwonić foowięcej lub mniej razy niż inne)

Witruwiusz
źródło