Почему этот метод печатает 4?

Мне было интересно, что произойдет, когда вы попытаетесь поймать StackOverflowError и предложили следующий метод:

class RandomNumberGenerator {

    static int cnt = 0;

    public static void main(String[] args) {
        try {
            main(args);
        } catch (StackOverflowError ignore) {
            System.out.println(cnt++);
        }
    }
}

Теперь мой вопрос:

Почему этот метод печатает '4'?

Я подумал, возможно, это потому, что System.out.println() требуется 3 сегмента в стеке вызовов, но я не знаю, откуда приходит номер 3. Когда вы смотрите на исходный код (и байт-код) System.out.println(), он обычно приведет к гораздо более сложным вызовам метода, чем 3 (так что 3 сегмента в стеке вызовов будет недостаточным). Если это связано с оптимизацией, применяемой Hotspot VM (метод inline), мне интересно, будет ли результат отличаться на другой VM.

Изменить

Поскольку вывод, кажется, очень специфичен JVM, я получаю результат 4, используя Java (TM) SE Runtime Environment (сборка 1.6.0_41-b02)
Java HotSpot (TM) 64-разрядная серверная VM (сборка 20.14-b01, смешанный режим)


Объяснение, почему я думаю, что этот вопрос отличается от Понимание стека Java:

Мой вопрос не в том, почему существует cnt > 0 (очевидно, потому что System.out.println() требует размера стека и бросает другой StackOverflowError, прежде чем что-то будет напечатано), но почему оно имеет конкретное значение 4, соответственно 0,3, 8,55 или что-то еще в других системах.

Ответ 1

Я думаю, что другие неплохо объяснили, почему cnt > 0, но не хватает подробностей о том, почему cnt = 4, и почему cnt так сильно варьируется среди разных настроек. Я попытаюсь заполнить эту пустоту здесь.

Пусть

  • X - общий размер стека
  • M - пространство стека, используемое при первом входе в основное меню
  • R - увеличение пространства стека при каждом входе в основной
  • P - пространство стека, необходимое для запуска System.out.println

Когда мы впервые попадаем в главное, оставшееся пространство X-M. Каждый рекурсивный вызов занимает R больше памяти. Таким образом, для 1 рекурсивного вызова (1 больше, чем оригинала) использование памяти равно M + R. Предположим, что StackOverflowError вызывается после C успешных рекурсивных вызовов, то есть M + C * R <= X и M + C * (R + 1) > X. Во время первого StackOverflowError осталось X-M-C * R.

Чтобы иметь возможность запускать System.out.prinln, нам нужно количество P, оставшееся в стеке. Если так получится, что X - M - C * R >= P, то 0 будет напечатано. Если P требует больше места, то мы удаляем кадры из стека, получая R-память за счет cnt ++.

Когда println, наконец, может работать, X - M - (C - cnt) * R >= P. Итак, если P велико для конкретной системы, то cnt будет большим.

Давайте рассмотрим это с некоторыми примерами.

Пример 1: Предположим

  • X = 100
  • M = 1
  • R = 2
  • P = 1

Тогда C = пол ((X-M)/R) = 49, а cnt = потолок ((P - (X - M - C * R))/R) = 0.

Пример 2: Предположим, что

  • X = 100
  • M = 1
  • R = 5
  • P = 12

Тогда C = 19 и cnt = 2.

Пример 3: Предположим, что

  • X = 101
  • M = 1
  • R = 5
  • P = 12

Тогда C = 20 и cnt = 3.

Пример 4: Предположим, что

  • X = 101
  • M = 2
  • R = 5
  • P = 12

Тогда C = 19 и cnt = 2.

Таким образом, мы видим, что и система (M, R, и P), и размер стека (X) влияют на cnt.

В качестве примечания стороны не имеет значения, сколько пространства catch требуется для запуска. Пока не хватает места для catch, тогда cnt не будет увеличиваться, поэтому внешних эффектов не будет.

ИЗМЕНИТЬ

Я беру обратно то, что я сказал о catch. Он играет определенную роль. Предположим, что для начала требуется T-пространство. cnt начинает увеличиваться, когда оставшееся пространство больше, чем T, и println выполняется, когда оставшееся пространство больше T + P. Это добавляет дополнительный шаг к вычислениям и дополнительно мутирует уже мутный анализ.

ИЗМЕНИТЬ

Наконец-то я нашел время, чтобы провести некоторые эксперименты, чтобы поддержать мою теорию. К сожалению, теория, похоже, не соответствует экспериментам. То, что на самом деле происходит, очень отличается.

Настройка эксперимента: Сервер Ubuntu 12.04 с по умолчанию java и default-jdk. Xss начиная с 70 000 с шагом 1 байт до 460 000.

Результаты доступны по адресу: https://www.google.com/fusiontables/DataSource?docid=1xkJhd4s8biLghe6gZbcfUs3vT5MpS_OnscjWDbM Я создал другую версию, где удаляется каждая повторяющаяся точка данных. Другими словами, показаны только те точки, которые отличаются от предыдущих. Это облегчает просмотр аномалий. https://www.google.com/fusiontables/DataSource?docid=1XG_SRzrrNasepwZoNHqEAKuZlHiAm9vbEdwfsUA

Ответ 2

Это жертва неудачного рекурсивного вызова. Поскольку вам интересно, почему значение cnt меняется, это связано с тем, что размер стека зависит от платформы. Java SE 6 в Windows имеет размер стека по умолчанию 320k в 32-разрядной виртуальной машине и 1024k в 64-разрядной виртуальной машине. Вы можете прочитать здесь.

Вы можете запускать различные размеры стека, и перед переполнением стека вы увидите разные значения cnt -

java -Xss1024k RandomNumberGenerator

Вы не видите, что значение cnt печатается несколько раз, даже если значение больше 1, потому что ваш оператор печати также бросает ошибку, которую вы можете отлаживать, чтобы быть уверенным через Eclipse или другие IDE.

Вы можете изменить код на следующий, чтобы отлаживать выполнение каждой инструкции, если вы предпочитаете -

static int cnt = 0;

public static void main(String[] args) {                  

    try {     

        main(args);   

    } catch (Throwable ignore) {

        cnt++;

        try { 

            System.out.println(cnt);

        } catch (Throwable t) {   

        }        
    }        
}

UPDATE:

По мере того, как это получает гораздо больше внимания, давайте еще один пример, чтобы сделать вещи более ясными -

static int cnt = 0;

public static void overflow(){

    try {     

      overflow();     

    } catch (Throwable t) {

      cnt++;                      

    }

}

public static void main(String[] args) {

    overflow();
    System.out.println(cnt);

}

Мы создали другой метод с именем overflow, чтобы выполнить плохую рекурсию и удалили оператор println из блока catch, чтобы он не запускал другой набор ошибок при попытке печати. Это работает так, как ожидалось. Вы можете попробовать поставить System.out.println(cnt); после cnt ++ выше и скомпилировать. Затем выполните несколько раз. В зависимости от вашей платформы вы можете получить разные значения cnt.

Вот почему мы вообще не ломаем ошибок, потому что тайна в коде не фантазия.

Ответ 3

Поведение зависит от размера стека (который можно установить вручную с помощью Xss. Размер стека является специфичным для архитектуры. Из JDK 7 исходный код:

//Размер стека по умолчанию для Windows определяется исполняемым файлом (java.exe
//имеет значение по умолчанию 320K/1MB [32 бит /64 бит]). В зависимости от версии Windows, изменение //ThreadStackSize в ненулевое значение может существенно повлиять на использование памяти.
//См. Комментарии в os_windows.cpp.

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

Сколько раз повторяется? - Ну, это зависит от того, когда JVM думает, что это больше не stackoverflow. И это зависит от размера стека каждого вызова функции (трудно найти) и Xss. Как упоминалось выше, общий размер и размер каждого вызова функции (в зависимости от размера страницы памяти и т.д.) Зависят от платформы. Отсюда и другое поведение.

Вызов java вызова с помощью -Xss 4M дает мне 41. Следовательно, корреляция.

Ответ 4

Я думаю, что отображаемый номер - это время, в течение которого вызов System.out.println вызывает исключение Stackoverflow.

Вероятно, это зависит от реализации println и количества вызовов стекирования, которые он сделал в нем.

В качестве иллюстрации:

Вызов main() вызывает вызов Stackoverflow при вызове i. Вызов i-1 основного catch исключает и вызывает println, которые вызывают второй Stackoverflow. cnt получить приращение до 1. Теперь вызов i-2 основного улова вызывает исключение и вызывает println. В println метод называется триггером третьего исключения. cnt получить приращение до 2. это продолжается до тех пор, пока println не сможет выполнить весь необходимый вызов и, наконец, отобразит значение cnt.

Это зависит от фактической реализации println.

Для JDK7 либо он обнаруживает циклический вызов, либо генерирует исключение раньше либо он сохраняет некоторый ресурс стека, либо генерирует исключение, прежде чем достигнуть предела, чтобы дать место для логики исправления, либо реализация println не выполняет вызовы либо Операция ++ выполняется после вызова println, таким образом, путем исключения по исключению.

Ответ 5

  • main рекурсирует сам по себе, пока он не переполнит стек с глубиной рекурсии R.
  • Запускается блок catch на глубине рекурсии R-1.
  • Блок catch при глубине рекурсии R-1 оценивает cnt++.
  • Блок catch на глубине R-1 вызывает println, ставя старое значение cnt в стек. println будет внутренне вызывать другие методы и использует локальные переменные и вещи. Все эти процессы требуют пространства стека.
  • Поскольку стек уже ограничивал лимит, а для вызова/выполнения println требуется пространство стека, новое переполнение стека запускается на глубине R-1 вместо глубины R.
  • Шаги 2-5 повторяются снова, но при глубине рекурсии R-2.
  • Шаги 2-5 повторяются снова, но при глубине рекурсии R-3.
  • Шаги 2-5 повторяются снова, но при глубине рекурсии R-4.
  • Шаги 2-4 повторяются снова, но при глубине рекурсии R-5.
  • Так получилось, что для завершения println достаточно пространства стека (обратите внимание, что это детализация реализации, она может меняться).
  • cnt был добавлен на глубину R-1, R-2, R-3, R-4 и, наконец, в R-5. Пятый пост-приращение возвратил четыре, что и было напечатано.
  • С main успешно завершен на глубине R-5, весь стек раскручивается, не запуская больше блоков catch, и программа завершается.

Ответ 6

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

Во-первых, нам нужно знать, когда будет выброшено StackOverflowError. Фактически, стек для потока Java хранит фреймы, содержащие все данные, необходимые для вызова метода и возобновления. Согласно Java Language Specifications для JAVA 6 при вызове метода

Если для создания такого кадра активации недостаточно памяти, генерируется StackOverflowError.

Во-вторых, мы должны четко указать, что "для создания такого кадра активации недостаточно памяти". Согласно спецификации виртуальной машины Java для JAVA 6,

Кадры

могут быть выделены в кучу.

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

Теперь вернемся к вопросу. Из вышеизложенного мы можем знать, что когда метод выполняется, он может стоить столько же пространства стека. И для вызова System.out.println (может) требуется 5 уровней вызова метода, поэтому необходимо создать 5 кадров. Затем, когда StackOverflowError выбрасывается, он должен вернуться назад 5 раз, чтобы получить достаточное пространство стека для хранения ссылок 5 кадров. Следовательно, 4 печатается. Почему бы не 5? Потому что вы используете cnt++. Измените его на ++cnt, а затем вы получите 5.

И вы заметите, что когда размер стека достигнет высокого уровня, вы получите 50 иногда. Это потому, что тогда необходимо принять во внимание количество доступного пространства кучи. Когда размер стека слишком велик, возможно, пространство кучи закончится до стека. И (возможно) фактический размер кадров стека System.out.println составляет примерно 51 раз main, поэтому он возвращается 51 раз и печатает 50.

Ответ 7

Это не совсем ответ на вопрос, но я просто хотел добавить что-то к исходному вопросу, с которым я столкнулся, и как я понял проблему:

В исходной задаче исключение попадает туда, где это было возможно:

Например, с jdk 1.7 он попадает на первое место вхождения.

но в более ранних версиях jdk похоже, что исключение не попадает в первую очередь, следовательно, 4, 50 и т.д.

Теперь, если вы удалите блок catch try следующим образом

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

Затем вы увидите все значения cnt ant заброшенных исключений (на jdk 1.7).

Я использовал netbeans для вывода вывода, так как cmd не отображает весь вывод и исключение.