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


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

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

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

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

6 322

6 ответов:

Он не работает вечно. Каждое переполнение стека приводит к перемещению кода в блок 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 требуется вдвое больше времени, чем глубина стека может быть 10000 или больше. Если вы можете сделать 10,000,000 звонков в секунду, это займет 10^3003 секунд или дольше, чем возраст Вселенной.

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

попробуйте выполнить следующий код:

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

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

наконец-то

исключение в потоке" main " java.ленг.Исключение: тест! при испытаниях.главный (тест.java: 6)

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

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

the правильный ответ на вопрос.

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

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 try-calls (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)

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

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 бросается на некоторые слои выше, так что вы можете сделать дополнительные шаги рекурсии, пока вы не нажмете другое исключение, и так далее. Это бесконечный "цикл".

кажется, что программа работает вечно; на самом деле она завершается, но она занимает экспоненциально больше времени, чем больше пространства стека у вас есть. Чтобы доказать, что он заканчивается, я написал программу, которая сначала истощает большую часть доступного пространства стека, а затем вызывает 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 больше или меньше раз, чем другие)