Чому цей метод друкує 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 сегмента в стеці виклику буде недостатньо). Якщо це через оптимізацію, яку застосовує VM Hotspot (метод вбудовування), мені цікаво, чи був би результат іншим для VM.

Редагувати :

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


Пояснення, чому я вважаю, що це питання відрізняється від Розуміння стеку Java :

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


4
У моїй місцевій області я отримую "0", як очікувалося.
Редді

2
Це може стосуватися багатьох архітектурних речей. Тож краще опублікуйте свій результат у версії jdk. Для мене вихід 0 на jdk 1.7
Локеш,

3
Я дістався 5, 6а 38з Java 1.7.0_10
Кон

8
@Elist Не буде таким самим результатом, коли ви робите хитрощі, пов’язані з базовою архітектурою;)
m0skit0

3
@flrnb Це просто стиль, який я використовую для вишивки брекетів. Я полегшує мені знати, де починаються і закінчуються умови та функції. Ви можете змінити його, якщо хочете, але, на мою думку, це читається таким чином.
syb0rg

Відповіді:


41

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

Дозволяти

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

Коли ми вперше потрапимо в основний, простір, що залишився, - це 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: Припустимо

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

Тоді C = пол ((XM) / R) = 49, а cnt = стеля ((P - (X - M - C * R)) / R) = 0.

Приклад 2: Припустимо, що

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

Тоді C = 19, а cnt = 2.

Приклад 3: Припустимо, що

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

Тоді C = 20, а cnt = 3.

Приклад 4: Припустимо, що

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

Тоді C = 19, а cnt = 2.

Таким чином, ми бачимо, що і система (M, R, і P), і розмір стека (X) впливають на cnt.

Як бічна примітка, не важливо, скільки місця catchпотрібно для початку. Поки не буде достатньо місця для catch, тоді cnt не збільшиться, тому немає зовнішніх ефектів.

EDIT

Я повертаю назад те, про що говорив catch. Це відіграє певну роль. Припустимо, для запуску потрібна кількість T місця. cnt починає збільшуватись, коли простір, що залишився, перевищує T іprintln запускається, коли простір, що залишився, перевищує T + P. Це додає додатковий крок до обчислень та додатково замулює вже мутний аналіз.

EDIT

Я нарешті знайшов час провести кілька експериментів, щоб підкріпити свою теорію. На жаль, теорія, схоже, не відповідає експериментам. Те, що насправді відбувається, дуже відрізняється.

Налаштування експерименту: сервер Ubuntu 12.04 з типовою java та 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 -Xss)?
flrnb

@flrnb M, R і P є специфічними для системи. Ви не можете їх легко змінити. Я очікую, що вони також різняться між деякими випусками
Джон Ценг

Тоді чому я отримую різні результати, змінюючи Xss (він же X)? Зміна X з 100 на 10000, враховуючи, що M, R і P залишаються однаковими, не повинні впливати на cnt відповідно до вашої формули, чи я помиляюся?
flrnb

@flrnb X сам змінює cnt через дискретний характер цих змінних. Приклади 2 і 3 відрізняються лише на X, але cnt відрізняється.
Джон Ценг

1
@JohnTseng Я також вважаю вашу відповідь найбільш зрозумілою і найповнішою на сьогоднішній день - все одно, мені було б дуже цікаво, як виглядає стек насправді в момент StackOverflowErrorкидання, і як це впливає на результат. Якщо він містив лише посилання на фрейм стека на купі (як запропонував Джей), то вихід повинен бути досить передбачуваним для даної системи.
flrnb

20

Це жертва поганого рекурсивного дзвінка. Як вам цікаво, чому значення cnt змінюється, це тому, що розмір стека залежить від платформи. Java SE 6 для Windows має типовий розмір стека 320k у 32-бітному VM та 1024k у 64-розрядному VM. Більше ви можете прочитати тут .

Ви можете запустити, використовуючи різні розміри стека, і ви побачите різні значення cnt, перш ніж стек переллється-

java -Xss1024k RandomNumberGenerator

Ви не бачите, що значення cnt друкується кілька разів, навіть незважаючи на те, що значення вище, ніж інколи, тому що ваш оператор друку також викидає помилку, яку ви можете налагодити, щоб бути впевненою через 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кидається, помилка потрапляє у блок лову. Ось println()ще один виклик стека, який знову видає виняток. Це повторюється.

Скільки разів це повторюється? - Ну, це залежить від того, коли JVM вважає, що це більше не є стартовим потоком. І це залежить від розміру стека кожного виклику функції (важко знайти) та Xss. Як було сказано вище, загальний розмір та розмір кожного виклику функції за замовчуванням (залежить від розміру сторінки пам'яті тощо) залежить від платформи. Звідси різна поведінка.

Здійснення javaдзвінка з -Xss 4Mмене дає мені 41. Звідси кореляція.


4
Я не розумію, чому розмір стека повинен впливати на результат, оскільки він вже перевищений, коли ми намагаємося надрукувати значення cnt. Таким чином, єдина відмінність могла виникнути від "розміру стека кожного виклику функції". І я не розумію, чому це має відрізнятися між двома машинами, що працюють в одній версії JVM.
flrnb

Точну поведінку можна отримати лише з джерела JVM. Але причиною могло бути це. Пам'ятайте, навіть catchце блок, який займає пам'ять на стеці. Скільки пам’яті займає кожен виклик методу, невідомо. Коли стек очищається, ви додаєте ще один блок catchі так. Це може бути поведінка. Це лише спекуляція.
Джатін

А розмір штабеля може відрізнятися у двох різних машинах. Розмір стека залежить від багатьох факторів на основі ОС, а саме розміру сторінки пам'яті тощо
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таким чином , виклик байпас по виключенню.


Це те, що я мав на увазі під «розумінням, можливо, це тому, що System.out.println потребує 3 сегменти на стеці викликів», - але я був спантеличений, чому саме це число, і тепер я ще більше спантеличений, чому число так сильно відрізняється між різними (віртуальні) машини
flrnb

Я частково погоджуюся з цим, але те, що я не погоджуюсь, полягає у твердженні "залежно від фактичної реалізації println". Це стосується розміру стека в кожному jvm, а не реалізації.
Джатін

6
  1. mainрекурсує на собі, поки не переповнить стек на глибині рекурсії R.
  2. Блок лову на глибині рекурсії R-1запускається.
  3. Блок лову на глибині рекурсії R-1оцінюєcnt++ .
  4. Блок лову на глибині R-1виклику println, розміщуючи cntна стеці старе значення. 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, весь стек розкручується без запуску більше блоків лову, і програма завершується.

1

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

По-перше, ми повинні знати, коли StackOverflowErrorбуде кинутий заповіт. Фактично, стек для 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), але якщо рішення було таким простим, чому результати будуть настільки варіювати на різних платформах та реалізація VM?
flrnb

@flrnb Це тому, що різні платформи можуть впливати на розмір кадру стека, а інша версія jre вплине на реалізацію System.out.printабо стратегію виконання методу. Як описано вище, реалізація VM також впливає на те, де буде фактично зберігатися кадр стека.
Джей

0

Це не зовсім відповідь на питання, але я просто хотів щось додати до початкового питання, на яке я натрапив і як я зрозумів проблему:

В оригінальній проблемі виняток потрапляє там, де це було можливо:

Наприклад, з jdk 1.7 він потрапляє на перше місце виникнення.

але в попередніх версіях jdk схоже, що виняток не потрапляє на перше місце виникнення, отже, 4, 50 тощо.

Тепер, якщо ви видалите блок спробу лову наступним чином

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

Тоді ви побачите всі значення cntмурашиних винятків (на jdk 1.7).

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.