Dlaczego ta metoda drukuje 4?

Zastanawiałem się, co się dzieje, gdy próbujesz złapać StackOverflowError i wymyśliłem następującą metodę:

class RandomNumberGenerator {

    static int cnt = 0;

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

Teraz moje pytanie:

Dlaczego ta metoda drukuje "4"?

Myślałem, że może dlatego, że System.out.println() potrzebuje 3 segmentów na stosie połączeń, ale nie wiem, skąd pochodzi numer 3. Gdy spojrzysz na kod źródłowy (i Bajt) System.out.println(), zwykle prowadzi to do znacznie większej liczby wywołań metody niż 3 (tak więc 3 segmenty na stosie wywołań nie byłyby wystarczające). Jeśli to z powodu optymalizacji stosuje się hotspot VM( metoda inlining), zastanawiam się, czy wynik byłby inny na innej maszynie wirtualnej.

Edit :

Ponieważ wyjście wydaje się być wysoce specyficzne dla JVM, otrzymuję wynik 4 używając
Java (TM) SE Runtime Environment (build 1.6.0_41-b02)
Java HotSpot (TM) 64-Bit Server VM (build 20.14-B01, mixed mode)


wyjaśnienie dlaczego myślę, że to pytanie różni się od zrozumienia stos Java:

Moje pytanie nie dotyczy tego, dlaczego istnieje cnt > 0 (oczywiście dlatego, że System.out.println() wymaga rozmiaru stosu i rzuca inny StackOverflowError zanim coś zostanie wydrukowane), ale dlaczego ma szczególną wartość 4, odpowiednio 0,3,8,55 lub coś innego na innych systemach.

Author: Community, 2013-07-24

7 answers

Myślę, że inni zrobili dobrą robotę wyjaśniając, dlaczego cnt > 0, ale nie ma wystarczająco dużo szczegółów dotyczących tego, dlaczego cnt = 4 i dlaczego cnt różni się tak bardzo między różnymi ustawieniami. Spróbuję wypełnić tę pustkę.

Let

  • x jest całkowitą wielkością stosu
  • M będzie przestrzenią stosu używaną przy pierwszym wejściu do main
  • R będzie zwiększaniem przestrzeni stosu za każdym razem, gdy wejdziemy do main
  • P będzie przestrzenią stosu niezbędną do uruchomienia System.out.println

Kiedy po raz pierwszy wejdziemy do main, przestrzeń pozostająca nad Jest X-M. każde wywołanie rekurencyjne zajmuje R więcej pamięci. Tak więc dla 1 wywołania rekurencyjnego (1 więcej niż oryginalnego), użycie pamięci to M + R. Załóżmy, że po pomyślnym wywołaniu rekurencyjnym C zostanie wyrzucony Stacoverflowerror, czyli M + C * R X. w czasie pierwszego Stacoverflowerror zostaje pamięć X - m - c * r.

Aby móc uruchomić System.out.prinln, potrzebujemy P ilość miejsca na stosie. Jeśli tak zdarza się, że X-M - C * R >= P, wtedy zostanie wydrukowane 0. Jeśli P wymaga więcej miejsca, usuwamy ramki ze stosu, zyskując pamięć R kosztem cnt++.

Kiedy println jest w stanie uruchomić, x - M - (C - cnt) * R >= P. więc jeśli p jest duże dla danego systemu, wtedy cnt będzie Duże.

Spójrzmy na to z kilkoma przykładami.

Przykład 1: Załóżmy, Że

  • X = 100
  • M = 1
  • R = 2
  • P = 1
Następnie C = podłoga((X-M)/R) = 49, a cnt = sufit ((P - (X - M - C*R)) / R) = 0.

Przykład 2: Załóżmy, że

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

Następnie C = 19, a cnt = 2.

Przykład 3: Załóżmy, że

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

Następnie C = 20, a cnt = 3.

Przykład 4: Załóżmy, że

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

Następnie C = 19, a cnt = 2.

Widzimy więc, że zarówno system (M, R I P), jak i rozmiar stosu (X) wpływają na cnt.

Na marginesie, nie ma znaczenia, ile miejsca potrzeba na start. Dopóki nie ma wystarczającej ilości miejsca na catch, cnt nie zwiększy się, więc nie ma efektów zewnętrznych.

EDIT

Cofam to, co powiedziałem o catch. Informatyka odgrywa pewną rolę. Załóżmy, że wymaga T ilość miejsca, aby rozpocząć. cnt zaczyna się zwiększać, gdy pozostała przestrzeń jest większa niż T, i println działa, gdy pozostała przestrzeń jest większa niż T + P. dodaje to dodatkowy krok do obliczeń i dalej mętnieje już błotnistej analizy.

EDIT

W końcu znalazłem czas, aby przeprowadzić kilka eksperymentów, aby potwierdzić moją teorię. Niestety teoria nie pasuje do eksperymentów. To, co się naprawdę dzieje, jest bardzo inaczej.

Konfiguracja eksperymentu: Ubuntu 12.04 server z domyślną Javą i domyślną-jdk. Xss zaczyna się od 70 000 przy przyrostach 1-bajtowych do 460 000.

Wyniki są dostępne na stronie: https://www.google.com/fusiontables/DataSource?docid=1xkJhd4s8biLghe6gZbcfUs3vT5MpS_OnscjWDbM Stworzyłem inną wersję, w której każdy powtarzający się punkt danych jest usuwany. Innymi słowy, pokazane są tylko punkty, które różnią się od poprzednich. Ułatwia to dostrzeganie anomalii. https://www.google.com/fusiontables/DataSource?docid=1XG_SRzrrNasepwZoNHqEAKuZlHiAm9vbEdwfsUA

 41
Author: John Tseng,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-07-30 15:19:11

To jest ofiara złego wywołania rekurencyjnego. Zastanawiasz się, dlaczego wartość cnt jest różna, ponieważ rozmiar stosu zależy od platformy. Java SE 6 w systemie Windows ma domyślny rozmiar stosu 320K w 32-bitowej maszynie wirtualnej i 1024k w 64-bitowej maszynie wirtualnej. Możesz przeczytać więcej tutaj .

Możesz uruchomić używając różnych rozmiarów stosu i zobaczysz różne wartości cnt przed przepełnieniem stosu-

Java-Xss1024k RandomNumberGenerator

Nie widzisz, że wartość cnt jest drukowana wiele razy, nawet jeśli wartość jest czasami większa niż 1, ponieważ instrukcja print również wyświetla błąd, który możesz debugować, aby upewnić się przez Eclipse lub inne IDE.

Możesz zmienić kod na następujący, aby debugować wykonanie instrukcji, jeśli wolisz -

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:

Ponieważ to coraz więcej uwagi, miejmy inny przykład do zrobienia things clearer -

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);

}

Stworzyliśmy inną metodę o nazwie overflow, Aby wykonać złą rekursję i usunęliśmy polecenie println z bloku catch, aby nie zaczął wyrzucać kolejnego zestawu błędów podczas próby wydrukowania. To działa zgodnie z oczekiwaniami. Możesz spróbować umieścić System.Wynocha.println (cnt); Instrukcja po cnt++ powyżej i skompilować. Następnie uruchom wiele razy. W zależności od platformy możesz otrzymać różne wartości cnt .

Dlatego generalnie nie wyłapujemy błędów, ponieważ tajemnica w kodzie nie jest fantazją.

 20
Author: Sajal Dutta,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-07-25 08:09:51

Zachowanie jest zależne od rozmiaru stosu (który można ustawić ręcznie za pomocą Xss. Rozmiar stosu jest specyficzny dla architektury. Z JDK 7 kod źródłowy :

/ / domyślny rozmiar stosu w systemie Windows jest określany przez plik wykonywalny (java.exe
// ma domyślną wartość 320K/1MB [32bit / 64bit]). W zależności od wersji systemu Windows, zmiana
// ThreadStackSize to non-zero może mieć znaczący wpływ na zużycie pamięci.
// Zobacz komentarze w os_windows.cpp.

Więc kiedy StackOverflowError jest wyrzucony, błąd jest przechwytywany w bloku catch. Tutaj println() jest kolejnym wywołaniem stosu, które ponownie wyrzuca wyjątek. To się powtarza.

ile razy się powtarza? - cóż, to zależy od tego, kiedy JVM myśli, że nie jest już stackoverflow. I to zależy od rozmiaru stosu każdego wywołania funkcji (trudnego do znalezienia) i Xss. Jak wspomniano powyżej domyślny Całkowity rozmiar i rozmiar każdego wywołania funkcji (zależy od wielkości strony pamięci itp) jest specyficzna dla platformy. Stąd inne zachowanie.

Wywołanie java wywołanie z -Xss 4M daje mi 41. Stąd korelat.

 13
Author: Jatin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-07-30 19:41:53

Myślę, że liczba wyświetlana jest liczbą czasu, w którym System.out.println wywołanie rzuca wyjątek Stackoverflow.

To prawdopodobnie zależy od implementacji println i numeru wywołania stosu w nim wykonanego.

Jako ilustracja:

Wywołanie main() wywołuje wyjątek Stackoverflow przy wywołaniu i. Wywołanie i-1 głównego wychwytuje wyjątek i wywołanie println, które uruchamia drugi Stackoverflow. cnt uzyskaj przyrost do 1. I-2 wywołanie main catch teraz wyjątek i wywołanie println. W println metoda nazywa się wyzwalaniem trzeciego wyjątku. cnt uzyskaj przyrost do 2. trwa to do momentu, aż {[2] } wykona wszystkie potrzebne wywołania i wyświetli wartość cnt.

Jest to zależne od rzeczywistej implementacji println.

Dla jdk7 albo wykrywa wywołanie cykliczne i wyrzuca wyjątek wcześniej albo zachowuje jakiś zasób stosu i wyrzuca wyjątek przed osiągnięciem limitu, aby dać trochę miejsca na logikę naprawy albo implementacja println nie wykonuje połączeń, operacja ++ jest wykonywana po wywołaniu println, więc jest przekazywana przez wyjątek.

 6
Author: Kazaag,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-07-24 11:06:02
  1. main rekursuje się, dopóki nie przepełni stosu na głębokości rekursji R.
  2. blok catch na głębokości rekurencji R-1 jest uruchamiany.
  3. blok catch na głębokości rekurencji R-1 ocenia cnt++.
  4. blok catch na głębokości R-1 wywołuje println, umieszczając starą wartość cnt na stosie. println wewnętrznie wywoła inne metody i używa lokalnych zmiennych i rzeczy. Wszystkie te procesy wymagają przestrzeni stosu.
  5. ponieważ stos już wypasał limit, a wywołanie / wykonanie println wymaga przestrzeni stosu, nowe przepełnienie stosu jest wyzwalane na głębokości R-1 zamiast głębokości R.
  6. kroki 2-5 powtórzą się, ale na głębokości rekurencji R-2.
  7. kroki 2-5 powtórzą się, ale na głębokości rekurencji R-3.
  8. kroki 2-5 powtórzą się, ale na głębokości rekurencji R-4.
  9. kroki 2-4 powtórzą się, ale na głębokości rekurencji R-5.
  10. tak się składa, że jest teraz wystarczająco dużo miejsca na println do uzupełnienia (zauważ, że to jest szczegółem realizacji, może się różnić).
  11. cnt był post-inkrementowany na głębokościach R-1, R-2, R-3, R-4, i wreszcie na R-5. Piąty post-przyrost zwrócił cztery, czyli to, co zostało wydrukowane.
  12. z main zakończonym pomyślnie na głębokości R-5, cały stos rozwija się bez uruchamiania kolejnych bloków catch I Program się kończy.
 6
Author: Craig Gidney,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-07-24 16:59:45

Po dłuższym pokopaniu, nie mogę powiedzieć, że znalazłem odpowiedź, ale myślę, że teraz jest dość blisko.

Najpierw musimy wiedzieć, kiedy StackOverflowError zostanie wyrzucony. W rzeczywistości stos dla wątku java przechowuje ramki, które zawierają wszystkie dane potrzebne do wywołania metody i wznowienia. Zgodnie z specyfikacją języka Java dla JAVA 6 , wywołując metodę,

Jeśli nie ma wystarczającej ilości pamięci do utworzenia takiej ramki aktywacyjnej, Rzucono StackOverflowError.

Po drugie, powinniśmy wyjaśnić, co to jest " nie ma wystarczającej ilości pamięci, aby utworzyć taką ramkę aktywacyjną ". Zgodnie ze specyfikacją Java Virtual Machine dla Javy 6,

Ramki mogą być przydzielane jako sterty.

Tak więc, gdy ramka jest tworzona, powinno być wystarczająco dużo miejsca na stos, aby utworzyć ramkę stosu i wystarczająco dużo miejsca na stos, aby zapisać nowe odniesienie, które wskazuje na nową ramkę stosu, jeśli ramka jest przydzielona.

Wróćmy do pytania. Z powyższego możemy wiedzieć, że gdy metoda jest wykonywana, może ona po prostu kosztować taką samą ilość miejsca na stosie. Wywołanie System.out.println (may) wymaga 5 poziomu wywołania metody, więc należy utworzyć 5 ramek. Następnie, gdy StackOverflowError jest wyrzucany, musi się cofnąć 5 razy, aby uzyskać wystarczająco dużo miejsca na stos, aby przechowywać odniesienia do 5 ramek. Stąd 4 jest wydrukowane. Dlaczego nie 5? Ponieważ używasz cnt++. Zmień go na ++cnt, a wtedy otrzymasz 5.

I zauważysz, że gdy rozmiar stosu osiągnie wysoki poziom, czasami otrzymasz 50. Wynika to z faktu, że ilość dostępnej przestrzeni sterty musi być wtedy brana pod uwagę. Gdy rozmiar stosu jest zbyt duży, może przed stosem zabraknie miejsca na stercie. I (być może) rzeczywisty rozmiar ramek stosu System.out.println wynosi około 51 razy main, dlatego wraca 51 razy i drukuje 50.

 1
Author: Jay,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-07-24 18:38:30

To nie jest do końca odpowiedź na pytanie, ale chciałem tylko dodać coś do pierwotnego pytania, na które natknąłem się i jak zrozumiałem problem:

W oryginalnym problemie wyjątek jest wyłapywany tam, gdzie było to możliwe:

Na przykład z jdk 1.7 jest przechwytywany w pierwszym miejscu wystąpienia.

Ale we wcześniejszych wersjach jdk wygląda na to, że wyjątek nie jest złapany w pierwszym miejscu występowania stąd 4, 50 itd..

Teraz jeśli usuniesz spróbuj złapać blok w następujący sposób

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

Wtedy zobaczysz wszystkie wartości cnt oraz wyrzucone wyjątki (w jdk 1.7).

Użyłem netbeans, aby zobaczyć dane wyjściowe, ponieważ cmd nie wyświetli wszystkich danych wyjściowych i WYJĄTKÓW.

 0
Author: me_digvijay,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-07-24 09:27:33