Try-w końcu blokuje StackOverflowError

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 skutkuje StackOverflowError, ale uruchamianie foo() nie (program wydaje się działać w nieskończoność). Dlaczego?

Author: RPichioli, 2012-09-15

6 answers

Nie działa wiecznie. Każde przepełnienie stosu powoduje przeniesienie kodu do bloku finally. Problem w tym, że zajmie to naprawdę, naprawdę dużo czasu. 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()

Do pracy na każdym poziomie W końcu bloku zajmuje dwa razy więcej czasu niż głębokość stosu może być 10,000 lub więcej. Jeśli możesz wykonać 10 000 000 połączeń na sekundę, zajmie to 10^3003 sekund lub dłużej niż wiek wszechświat.

 326
Author: Peter Lawrey,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-09-15 17:17:52

Kiedy otrzymujesz wyjątek od wywołania foo() wewnątrz try, wywołujesz foo() z finally i zaczynasz ponownie rekurencyjnie. Gdy spowoduje to inny wyjątek, wywołasz foo() z innego wewnętrznego finally(), i tak dalej prawie ad infinitum.

 40
Author: ninjalj,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-09-15 17:02:32

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

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

Przekonasz się, że blok finally jest wykonywany przed wyrzuceniem wyjątku do poziomu powyżej. (Wyjście:

Wreszcie

Wyjątek w wątku "main" java.lang.Wyjątek: TEST! w teście.główna (test.java: 6)

To ma sens, jak w końcu nazywa się tuż przed zakończeniem metody. Oznacza to jednak, że gdy zdobędziesz ten pierwszy StackOverflowError, spróbuje go rzucić, ale w końcu musi wykonać najpierw uruchamia się foo() ponownie, co powoduje przepełnienie stosu i jako takie uruchamia się ponownie. Dzieje się to w nieskończoność, więc wyjątek nigdy nie jest drukowany.

W Twojej metodzie bar jednak, jak tylko wystąpi wyjątek, jest on po prostu wyrzucany prosto do poziomu powyżej i zostanie wydrukowany

 37
Author: Alex Coleman,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-09-15 15:54:39

W celu dostarczenia rozsądnych dowodów, że to ostatecznie zakończy się, oferuję następujący, raczej bezsensowny kod. Uwaga: Java nie jest moim językiem, przez żaden odcinek najbardziej żywej wyobraźni. Proponuję to tylko po to, aby poprzeć odpowiedź Piotra, która jest poprawną odpowiedzią na pytanie.

To próba symulowania warunków, co się dzieje, gdy wywołanie nie może się wydarzyć, ponieważ wprowadziłoby przepełnienie stosu. Wydaje mi się, że najtrudniej jest ludziom nie rozumieją, że wywołanie nie dzieje się wtedy, gdy nie może się zdarzyć.

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

Wynik tej małej bezsensownej kupy mazi jest następujący, a faktyczny wyłapany wyjątek może być zaskoczeniem; Oh, I 32 TRY-calls (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)
 25
Author: WhozCraig,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-09-18 22:08:17

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 wyjście, które 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 widzisz, przepływ stosu jest wyrzucany na niektóre warstwy powyżej, więc możesz wykonać dodatkowe kroki rekurencji, aż do uzyskania kolejnego wyjątku, i tak dalej. Jest to nieskończona "pętla".

 23
Author: Karoly Horvath,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-09-15 16:14:31

Program wydaje się działać w nieskończoność; w rzeczywistości kończy się, ale zajmuje wykładniczo więcej czasu, tym więcej miejsca na stosie. Aby udowodnić, że to się skoń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ą wywoływać foo więcej lub mniej razy niż inne)

 0
Author: Vitruvius,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-04-11 00:08:28