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

111

Мне было интересно, что происходит, когда вы пытаетесь поймать 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 (встраивание метода), мне интересно, будет ли результат отличаться на другой виртуальной машине.

Редактировать :

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


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

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

flrnb
источник
4
В моем местном я получаю «0», как и ожидалось.
Reddy
2
Это может касаться многих архитектурных вещей. Так что лучше опубликуйте свой вывод с версией jdk. Для меня вывод равен 0 на jdk 1.7
Локеш
3
Я получил 5, 6и 38с Java 1.7.0_10
Kon
8
@Elist Не будет такого же результата, когда вы проделываете трюки с базовой архитектурой;)
m0skit0
3
@flrnb Это просто стиль, который я использую, чтобы выровнять фигурные скобки. Так мне легче узнать, где условия и функции начинаются и заканчиваются. Вы можете изменить его, если хотите, но, на мой взгляд, это более читабельно.
syb0rg

Ответы:

41

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

Позволять

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

Когда мы впервые попадаем в main, остается пространство XM. Каждый рекурсивный вызов занимает на 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: предположим

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

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

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

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

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

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

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

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

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

  • Х = 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

Джон Ценг
источник
Спасибо за хорошее резюме, я думаю, все сводится к вопросу: что влияет на M, R и P (поскольку X можно установить с помощью параметра VM-option -Xss)?
flrnb
@flrnb M, R и P зависят от системы. Вы не можете легко их изменить. Я ожидаю, что они также будут различаться между некоторыми выпусками.
John Tseng
Тогда почему я получаю разные результаты, изменяя Xss (он же X)? Изменение X со 100 на 10000, учитывая, что M, R и P остаются прежними, не должно влиять на cnt в соответствии с вашей формулой, или я ошибаюсь?
flrnb
Только @flrnb X изменяет cnt из-за дискретного характера этих переменных. Примеры 2 и 3 различаются только по X, но cnt отличается.
John Tseng
1
@JohnTseng Я также считаю ваш ответ на данный момент наиболее понятным и полным - в любом случае, мне было бы очень интересно, как на самом деле выглядит стек в момент StackOverflowErrorвыброса и как это влияет на результат. Если он содержит только ссылку на кадр стека в куче (как предположил Джей), то вывод должен быть вполне предсказуемым для данной системы.
flrnb
20

Это жертва плохого рекурсивного вызова. Вам интересно, почему значение cnt меняется, потому что размер стека зависит от платформы. Java SE 6 в Windows имеет размер стека по умолчанию 320 КБ для 32-разрядной виртуальной машины и 1024 КБ для 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) {   

        }        
    }        
}

ОБНОВИТЬ:

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

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 .

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

Саджал Дутта
источник
13

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

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

Таким образом, когда StackOverflowErrorвызывается, ошибка перехватывается в блоке catch. Вот println()еще один вызов стека, который снова вызывает исключение. Это повторяется.

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

Звонок по javaтелефону -Xss 4Mдает мне 41. Отсюда корреляция.

Jatin
источник
4
Я не понимаю, почему размер стека должен влиять на результат, поскольку он уже превышен, когда мы пытаемся вывести значение cnt. Таким образом, единственное различие могло заключаться в «размере стека каждого вызова функции». И я не понимаю, почему это должно различаться между двумя машинами, на которых установлена ​​одна и та же версия JVM.
flrnb
Точное поведение можно получить только из источника JVM. Но причина могла быть в этом. Помните, что even catch- это блок, который занимает память в стеке. Неизвестно, сколько памяти занимает каждый вызов метода. Когда стек очищается, вы добавляете еще один блок catchи так далее. Это может быть поведение. Это только предположения.
Jatin
И размер стека может отличаться на двух разных машинах. Размер стека зависит от множества факторов, связанных с операционной системой, а именно от размера страницы памяти и т. Д.
Jatin
6

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

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

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

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

Тогда это зависит от фактической реализации println .

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

Kazaag
источник
Это то, что я имел в виду, говоря: «Я подумал, может быть, это потому, что System.out.println нужно 3 сегмента в стеке вызовов» - но я был озадачен, почему это именно это число, и теперь я еще больше озадачен, почему это число так сильно различается среди разных (виртуальные) машины
flrnb
Я частично согласен с этим, но я не согласен с утверждением `зависит от фактической реализации println`. Это связано с размером стека в каждом jvm, а не с реализацией.
Jatin
6
  1. main рекурсивно повторяется до тех пор, пока не переполнит стек на глубине рекурсии R .
  2. Блок catch на глубине рекурсии R-1 .
  3. R-1Оценивается блок catch на глубине рекурсии cnt++.
  4. Блок поймать на глубине R-1вызовов println, размещая cnt«S старого значения в стеке. printlnбудет внутренне вызывать другие методы и использовать локальные переменные и прочее. Все эти процессы требуют места в стеке.
  5. Поскольку стек уже достиг предела, а для вызова / выполнения printlnтребуется пространство стека, новое переполнение стека запускается на глубине, R-1а не на глубине R.
  6. Шаги 2–5 повторяются снова, но с глубиной рекурсии R-2.
  7. Шаги 2–5 повторяются снова, но с глубиной рекурсии R-3.
  8. Шаги 2–5 повторяются снова, но с глубиной рекурсии R-4.
  9. Шаги 2–4 повторяются снова, но с глубиной рекурсии R-5.
  10. Так получилось, что теперь в стеке достаточно места для printlnзавершения (обратите внимание, что это деталь реализации, она может отличаться).
  11. cntбыл пост-приращение на глубинах R-1, R-2, R-3, R-4, и , наконец,R-5 . Пятый постинкремент вернул четыре, что и было напечатано.
  12. При mainуспешном завершении на глубине R-5весь стек раскручивается без запуска дополнительных блоков catch, и программа завершается.
Крейг Гидни
источник
1

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

Во-первых, нам нужно знать, когда StackOverflowErrorбудет брошено a . Фактически, стек для потока Java хранит кадры, которые содержат все данные, необходимые для вызова метода и возобновления. Согласно спецификациям языка Java для 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.

Джей
источник
Моей первой мыслью было также подсчет уровней вызовов методов (и вы правы, я не обратил внимание на то, что я публикую инкремент cnt), но если бы решение было таким простым, почему бы результаты так сильно различались на разных платформах? и реализации ВМ?
flrnb
@flrnb Это потому, что разные платформы могут влиять на размер фрейма стека, а разные версии jre влияют на реализацию System.out.printили стратегию выполнения метода. Как описано выше, реализация виртуальной машины также влияет на место фактического хранения кадра стека.
Джей
0

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

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

Например с jdk 1.7 он отлавливается по первому месту возникновения.

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

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

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

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

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

me_digvijay
источник