Наконец, не выбрасывает исключение

Насколько я знаю, этот код должен бросать StackOverflowError, но это не так. Что может быть причиной?

public class SimpleFile {

    public static void main(String[] args) {
        System.out.println("main");

        try{
        SimpleFile.main(args);
            }

        catch(Exception e){
            System.out.println("Catch");
        }

        finally{
            SimpleFile.main(args);
        }
    }

}

Ответ 1

Error не является Exception. Таким образом, вылавливание любого исключения не будет захватывать StackOverflowError.

Итак, давайте начнем с фиксации "очевидной ошибки" - (этот код нецелесообразен, как указано позже в этом ответе):

    catch(Throwable e){
        System.out.println("Catch");
    }

Если вы внесете это изменение, вы обнаружите, что код все еще не печатает. Но он не печатает по совершенно другой причине...

Уловка любого ERROR (включая a StackOverflowError) сильно обескуражена. Но здесь вы не только поймаете один, вы ловите его, как это происходит в верхней части стека. Даже с вашим кодом (без вышеуказанного изменения) ошибка эффективно ломается блоком finally.

A StackOverflowError возникает, когда стек заполнен, и вы пытаетесь добавить к нему больше. Поэтому, когда вы поймаете ошибку, стек все еще заполнен. Вы не можете вызвать какой-либо метод (даже для печати на консоль), потому что стек заполнен. Таким образом, вторая StackOverflowError выбрасывается в catch, прежде чем она будет успешно распечатана.

Результатом этого является то, что он:

  • Улавливает ошибку
  • пытается распечатать ошибку.
  • вызывает другую ошибку, поскольку она не может печатать
  • вызывает finally, потому что, наконец, всегда вызывается.
  • вызывает другую ошибку, потому что она не может вызвать main
  • каскадирует ошибку обратно к предыдущему вызову, который работает с той же ошибкой.

Ключевым моментом здесь является то, что в конце концов он начнет печатать что-то. Но вызов print использует много пространства стека, и ваш код будет вынужден повторять и пропустить ошибки в вышеупомянутых пунктах в течение очень долгого времени, прежде чем он освободит достаточно пространства стека для печати. Согласно комментарию Хольгера. С помощью Oracles Java 8 поместить количество фреймов стека main, необходимых для фрейма стека println, близкого к 50.

250 = 1,125,899,906,842,624

Вот почему ВЫ ДОЛЖНЫ НИКОГДА НЕ ПОЛУЧИТЬ ОШИБКИ.

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

Ответ 2

На самом деле вы получили java.lang.Stackoverflow

Вы можете запустить этот пример кода:

public class SimpleFile {
    public static void main(String[] args) {
       System.out.println("main ");
       try{
          SimpleFile.main(args);
       }finally{
          try{
             SimpleFile.main(args);
          }catch(Error e2){
             System.out.println("finally");
             throw e2;
          }
       }
   }
}

PS

Подробнее: ваша программа печатает много сообщений main, после чего вы впервые получаете ошибку и завершаете блокировку. Это означает, что вы уменьшаете размер стека, и теперь вы можете что-то вызывать. Но вы вызываете себя в конце блока и снова получаете переполнение стека. Самое удивительное для меня было неустойчивое производство:

 main 
 main main finally
 main 
 main main finallyfinallyfinally
 main 
 main 

Ответ 3

Сначала у вас есть предложение catch, которое не ломает Error s:

catch(Exception e){
    System.out.println("Catch");
}

Так как Error не Exception s, это не ломает StackOverflowError, и оператор печати не будет выполнен. Если Error не улавливается, его трассировка стека будет напечатана по умолчанию обработчиком потока, если он когда-либо достигнет этой точки. Но у вас есть другое предложение:

finally{
    SimpleFile.main(args);
}

Код предложения finally всегда будет выполняться, когда блок try завершается, как обычно, так и исключительно. Поскольку ваш блок try содержит бесконечную рекурсию, он никогда не завершится нормально.

В исключительном случае, т.е. когда выбрано a StackOverflowError, действие finally снова войдет в бесконечную рекурсию, что может снова закончиться с StackOverflowError, но поскольку оно имеет тот же finally, он также снова войдет в бесконечную рекурсию.

Ваша программа в основном говорит: "Выполняйте бесконечную рекурсию, затем еще одну бесконечную рекурсию". Обратите внимание, что вы не можете отличить от печати "main", работает ли программа в первичной бесконечной рекурсии или одна из них запускается из блока finally (за исключением того, что разрыв строки может отсутствовать, если поток stackoverflow происходит прямо между выполнением println).

Итак, если мы предположим, что конкретный JVM имеет предел 1000 вложенных вызовов, ваша программа будет выполнять 2¹⁰⁰⁰ вызовы вашего метода main (количественное). Поскольку ваш метод main фактически ничего не делает, оптимизатор может избежать даже этого невероятного количества вызовов, но эта оптимизация также подразумевает, что требуемый размер стека исчезает, и, таким образом, становится возможным еще большее количество рекурсивных вызовов. Только реализация JVM, обеспечивающая преднамеренный лимит на поддерживаемое количество рекурсивных вызовов, независимо от фактически требуемого пространства стека, может принудить эту программу к завершению.

Но обратите внимание, что в случае бесконечной рекурсии нет гарантии получить a StackOverflowError вообще. Теоретически, JVM, имеющий бесконечное пространство стека, будет действительной реализацией. Это означает, что JVM, практически оптимизирующий рекурсивный код для запуска, не требуя дополнительного пространства стека, также будет действительным.

Итак, для типичной реализации, такой как JVM Oracles, практически невозможно, чтобы ваша программа когда-либо сообщала StackOverflowError. Они происходят, но затенены ваши последующие рекурсии в блоке finally, поэтому они никогда не сообщаются.

Ответ 4

Я сделал некоторые изменения в вашем коде и провел несколько тестов. Я до сих пор не могу понять ответ на ваш вопрос. Это, безусловно, окончательный блок, который вызывает полное утомленное поведение кода.

public class SimpleFile {

    static int i = 0;

    public static void main(String[] args) {
        int c = i++;
        System.out.println("main" + i);

        try {
            SimpleFile.main(args);
        }

        catch (Throwable e) {
            System.out.println("Catch" + e);
        }

        finally {
            if (i < 30945) {
                System.out.println("finally" + c);
                SimpleFile.main(args);
            }
        }
    }
}

И вышло это.. Я показываю только последние строки:

main30941
main30942finally30940
main30943finally30927
main30944
main30945
main30946
main30947Catchjava.lang.StackOverflowError

Я хочу доказать, что даже статические методы получают StackOverflowError, если они называются рекурсивно. И так как вы ловили исключение, которое никогда не поймало бы ошибку.

См. Вызвать главный рекурсивно вопрос для StackOverflowError, если main называется рекурсивно

Ответ 5

Статические методы являются постоянными, они не хранятся в стеке, а в куче. Код, который вы написали, просто вызывает один и тот же код снова и снова из кучи, поэтому он не будет бросать StackOverFlowError. Кроме того, строка внутри System.out.println("main"); сохраняется в том же месте, которое является постоянным. Несмотря на то, что вы вызывали код снова и снова, используется тот же объект строки, он не заполняет стек.

Я получил это объяснение по следующей ссылке:

http://www.oracle.com/technetwork/java/javase/memleaks-137499.html#gbyuu

3.1.2 Подробное сообщение: пространство PermGen Детальное сообщение. Поле PermGen указывает, что постоянное поколение заполнено. Постоянный генерация - это область кучи, где объекты класса и метода сохраняются. Если приложение загружает очень большое количество классов, то возможно, потребуется увеличить размер постоянного поколения, используя параметр -XX: MaxPermSize.

Внутренние объекты java.lang.String также хранятся в постоянных поколение. Класс java.lang.String поддерживает пул строк. Когда вызывается метод intern, метод проверяет пул, чтобы видеть если в пуле уже есть равная строка. Если есть, то метод intern возвращает его; иначе он добавляет строку в пул. В более точные термины, метод java.lang.String.intern используется для получить каноническое представление строки; результатом является ссылку на тот же экземпляр класса, который будет возвращен, если это строка появилась как буквальная. Если приложение ставит огромное количество строк, постоянное поколение, возможно, потребуется увеличить с его значение по умолчанию.

При возникновении такой ошибки текст String.intern или ClassLoader.defineClass может отображаться в верхней части трассировки стека который печатается.

Команда jmap -permgen выводит статистику для объектов в постоянное поколение, включая информацию о интернализованной строке экземпляры.