Zastanawiałem się, co się stanie, gdy spró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”?
Pomyślałem, że może to dlatego, że System.out.println()
potrzebuje 3 segmentów na stosie wywołań, ale nie wiem, skąd pochodzi numer 3. Kiedy patrzysz na kod źródłowy (i kod bajtowy) programu System.out.println()
, zwykle prowadzi to do znacznie większej liczby wywołań metod niż 3 (więc 3 segmenty na stosie wywołań nie byłyby wystarczające). Jeśli jest to spowodowane optymalizacjami, które stosuje Hotspot VM (metoda inlining), zastanawiam się, czy wynik byłby inny na innej VM.
Edytować :
Ponieważ dane wyjściowe wydają się być wysoce specyficzne dla JVM, otrzymuję wynik 4 przy użyciu
środowiska Java (TM) SE Runtime Environment (kompilacja 1.6.0_41-b02
) 64-bitowa maszyna wirtualna serwera Java HotSpot (TM) (wersja 20.14-b01, tryb mieszany)
Wyjaśnienie, dlaczego uważam, że to pytanie różni się od Zrozumienia stosu Java :
Moje pytanie nie dotyczy tego, dlaczego jest cnt> 0 (oczywiście, ponieważ 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 innym systemy.
źródło
5
,6
i38
Java 1.7.0_10Odpowiedzi:
Myślę, że inni wykonali 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 w różnych ustawieniach. Spróbuję tutaj wypełnić tę pustkę.
Pozwolić
System.out.println
Kiedy po raz pierwszy wchodzimy do main, pozostała przestrzeń to XM. Każde wywołanie rekurencyjne zajmuje R więcej pamięci. Więc dla 1 wywołania rekurencyjnego (o 1 więcej niż oryginał), użycie pamięci wynosi M + R. Załóżmy, że StackOverflowError jest generowany po pomyślnych wywołaniach rekurencyjnych w C, to znaczy M + C * R <= X i M + C * (R + 1)> X. W momencie pierwszego StackOverflowError pozostała pamięć X - M - C * R.
Aby móc biegać
System.out.prinln
, potrzebujemy P ilość wolnego miejsca na stosie. Jeśli tak się stanie, że X - M - C * R> = P, to zostanie wydrukowane 0. Jeśli P wymaga więcej miejsca, usuwamy ramki ze stosu, uzyskując pamięć R kosztem cnt ++.Kiedy w
println
końcu będzie można uruchomić, X - M - (C - cnt) * R> = P. Więc jeśli P jest duże dla konkretnego systemu, to cnt będzie duże.Spójrzmy na to z kilkoma przykładami.
Przykład 1: Załóżmy
Wtedy C = podłoga ((XM) / R) = 49, a cnt = sufit ((P - (X - M - C * R)) / R) = 0.
Przykład 2: Załóżmy, że
Wtedy C = 19 i cnt = 2.
Przykład 3: Załóżmy, że
Wtedy C = 20 i cnt = 3.
Przykład 4: Załóżmy, że
Wtedy C = 19 i cnt = 2.
Widzimy więc, że zarówno system (M, R i P), jak i rozmiar stosu (X) mają wpływ na cnt.
Na marginesie, nie ma znaczenia, ile miejsca
catch
potrzeba na rozpoczęcie. Dopóki nie ma na to miejscacatch
, cnt nie wzrośnie, więc nie ma efektów zewnętrznych.EDYTOWAĆ
Cofam to, o czym mówiłem
catch
. Odgrywa rolę. Załóżmy, że do uruchomienia potrzeba T miejsca. cnt zaczyna zwiększać się, gdy pozostała przestrzeń jest większa niż T iprintln
działa, gdy pozostała przestrzeń jest większa niż T + P. To dodaje dodatkowy krok do obliczeń i jeszcze bardziej zagmatwa i tak już mętną analizę.EDYTOWAĆ
W końcu znalazłem czas, aby przeprowadzić kilka eksperymentów, aby potwierdzić moją teorię. Niestety, teoria wydaje się nie zgadzać się z eksperymentami. To, co się właściwie dzieje, jest zupełnie inne.
Konfiguracja eksperymentu: serwer Ubuntu 12.04 z domyślnym java i default-jdk. Xss począwszy od 70 000 z przyrostem 1 bajta do 460 000.
Wyniki są dostępne pod adresem : https://www.google.com/fusiontables/DataSource?docid=1xkJhd4s8biLghe6gZbcfUs3vT5MpS_OnscjWDbM Stworzyłem kolejną wersję, w której każdy powtarzający się punkt danych jest usuwany. Innymi słowy, wyświetlane są tylko punkty różniące się od poprzednich. Dzięki temu łatwiej dostrzec anomalie. https://www.google.com/fusiontables/DataSource?docid=1XG_SRzrrNasepwZoNHqEAKuZlHiAm9vbEdwfsUA
źródło
StackOverflowError
rzucenia i jak wpływa to na wynik. Jeśli zawierało tylko odniesienie do ramki stosu na stercie (jak zasugerował Jay), to wynik powinien być dość przewidywalny dla danego systemu.To jest ofiara złego wywołania rekurencyjnego. Ponieważ zastanawiasz się, dlaczego wartość cnt jest różna, dzieje się tak, ponieważ rozmiar stosu zależy od platformy. Java SE 6 w systemie Windows ma domyślny rozmiar stosu 320 KB w 32-bitowej maszynie wirtualnej i 1024 KB w 64-bitowej maszynie wirtualnej. Możesz przeczytać więcej tutaj .
Możesz uruchomić z różnymi rozmiarami stosów, a zobaczysz różne wartości cnt, zanim stos się przepełni.
Nie widzisz, że wartość cnt jest drukowana wiele razy, mimo że czasami wartość jest większa niż 1, ponieważ instrukcja print również generuje błąd, który możesz debugować, aby mieć pewność, że za pomocą Eclipse lub innych IDE.
Możesz zmienić kod na następujący, aby debugować na wykonanie instrukcji, jeśli wolisz:
AKTUALIZACJA:
Ponieważ przyciąga to dużo więcej uwagi, posłużmy się innym przykładem, aby wszystko było jaśniejsze:
Stworzyliśmy inną metodę o nazwie overflow, aby wykonać złą rekurencję i usunęliśmy instrukcję println z bloku catch, aby nie zaczęła generować kolejnego zestawu błędów podczas próby drukowania. Działa to zgodnie z oczekiwaniami. Możesz spróbować umieścić System.out.println (cnt); instrukcja po cnt ++ powyżej i skompiluj. Następnie uruchom wiele razy. W zależności od platformy możesz uzyskać różne wartości cnt .
Dlatego generalnie nie łapiemy błędów, ponieważ tajemnica w kodzie nie jest fantazją.
źródło
Zachowanie zależy od rozmiaru stosu (który można ustawić ręcznie za pomocą
Xss
. Rozmiar stosu zależy od architektury. Z kodu źródłowego JDK 7 :Więc kiedy
StackOverflowError
jest rzucany, błąd jest przechwytywany w bloku catch. Otoprintln()
kolejne wywołanie stosu, które ponownie zgłasza wyjątek. To się powtarza.Ile razy to się powtarza? - Cóż, to zależy od tego, kiedy JVM myśli, że nie jest to już przepełnienie stosu. A to zależy od rozmiaru stosu każdego wywołania funkcji (trudne do znalezienia) i
Xss
. Jak wspomniano powyżej, domyślny całkowity rozmiar i rozmiar każdego wywołania funkcji (zależy od rozmiaru strony pamięci itp.) Zależy od platformy. Stąd inne zachowanie.Wywołanie
java
połączenia z-Xss 4M
daje mi41
. Stąd korelacja.źródło
catch
jest blokiem i zajmuje pamięć na stosie. Nie wiadomo, ile pamięci zajmuje każde wywołanie metody. Kiedy stos zostanie wyczyszczony, dodajesz jeszcze jeden blokcatch
i tak dalej. To może być zachowanie. To tylko spekulacje.Myślę, że wyświetlona liczba to czas, przez jaki
System.out.println
połączenie zgłaszaStackoverflow
wyjątek.Prawdopodobnie zależy to od implementacji
println
i liczby wywołań stosowych, które są w nim wykonywane.Jako ilustracja:
main()
Wywołanie wyzwalaczaStackoverflow
wyjątek na połączenia i. Wywołanie i-1 main przechwytuje wyjątek iprintln
wywołuje sekundęStackoverflow
.cnt
pobierz przyrost do 1. Wywołanie i-2 main catch teraz wyjątek i wywołanieprintln
. Wprintln
metodzie nazywa się wyzwalanie trzeciego wyjątku.cnt
uzyskać przyrost do 2. To kontynuuje, aż będzieprintln
można wykonać wszystkie potrzebne wywołania i ostatecznie wyświetlić wartośćcnt
.Zależy to wtedy od faktycznej implementacji
println
.W przypadku JDK7 albo wykrywa wywołanie cykliczne i zgłasza wyjątek wcześniej albo zachowuje trochę zasobów stosu i rzuca wyjątek przed osiągnięciem limitu, aby dać trochę miejsca na logikę naprawczą albo
println
implementacja nie wykonuje wywołań albo operacja ++ jest wykonywana poprintln
połączenie jest w ten sposób przez przejściu przez wyjątku.źródło
main
powtarza się do momentu przepełnienia stosu na głębokości rekurencjiR
.R-1
.R-1
Ocenia się blok catch na głębokości rekurencjicnt++
.R-1
wywołujeprintln
, umieszczająccnt
starą wartość na stosie.println
będzie wewnętrznie wywoływać inne metody i używać lokalnych zmiennych i rzeczy. Wszystkie te procesy wymagają miejsca na stosie.println
przekraczał limit, a wywołanie / wykonanie wymaga miejsca na stosie, nowe przepełnienie stosu jest wyzwalane na głębokościR-1
zamiast na głębokościR
.R-2
.R-3
.R-4
.R-5
.println
do zakończenia (zwróć uwagę, że jest to szczegół implementacji, może się różnić).cnt
był postinkrementowany na głębokościachR-1
,R-2
,R-3
,R-4
, a na koniec wR-5
. Piąty post-inkrementacja zwrócił cztery, czyli to, co zostało wydrukowane.main
pomyślnym zakończeniu na głębokościR-5
cały stos jest rozwijany bez uruchamiania kolejnych bloków catch i program kończy się.źródło
Po pewnym czasie nie mogę powiedzieć, że znalazłem odpowiedź, ale myślę, że jest już blisko.
Po pierwsze, musimy wiedzieć, kiedy
StackOverflowError
zostanie wyrzucony testament. W rzeczywistości stos wątku Java przechowuje ramki, które zawierają wszystkie dane potrzebne do wywołania metody i wznowienia. Zgodnie ze specyfikacjami języka Java dla JAVA 6 podczas wywoływania metodyPo drugie, powinniśmy wyjaśnić, co oznacza „ brak wystarczającej ilości pamięci do utworzenia takiej ramki aktywacji ”. Zgodnie ze specyfikacjami maszyn wirtualnych Java dla JAVA 6 ,
Tak więc po utworzeniu ramki powinno być wystarczająco dużo miejsca na sterty, aby utworzyć ramkę stosu i wystarczająco dużo miejsca na stosie, aby zapisać nowe odniesienie, które wskazuje na nową ramkę stosu, jeśli ramka jest przydzielona na sterty.
Wróćmy teraz do pytania. Z powyższego możemy wiedzieć, że gdy metoda jest wykonywana, może to po prostu kosztować taką samą ilość miejsca na stosie. A wywołanie
System.out.println
(może) wymagać 5 poziomów wywołania metody, więc należy utworzyć 5 ramek. Następnie, gdyStackOverflowError
zostanie wyrzucony, musi cofnąć się 5 razy, aby uzyskać wystarczająco dużo miejsca na stosie do przechowywania odwołań do 5 ramek. Stąd 4 jest drukowane. Dlaczego nie 5? Ponieważ używaszcnt++
. Zmień to na++cnt
, a otrzymasz 5.Zauważysz, że kiedy rozmiar stosu osiągnie wysoki poziom, czasami otrzymasz 50. Dzieje się tak, ponieważ wtedy należy wziąć pod uwagę ilość dostępnego miejsca na stercie. Kiedy rozmiar stosu jest zbyt duży, może zabraknie miejsca na stosie przed stosem. I (być może) rzeczywisty rozmiar klatek stosu
System.out.println
wynosi około 51 razymain
, więc cofa się 51 razy i drukuje 50.źródło
System.out.print
lub strategię wykonywania metody. Jak opisano powyżej, implementacja maszyny wirtualnej ma również wpływ na to, gdzie ramka stosu będzie faktycznie przechowywana.To nie jest dokładna odpowiedź na pytanie, ale chciałem tylko dodać coś do pierwotnego pytania, na które się natknąłem i jak zrozumiałem problem:
W pierwotnym problemie wyjątek jest wychwytywany tam, gdzie było to możliwe:
Na przykład w przypadku jdk 1.7 jest łapany w pierwszym miejscu wystąpienia.
ale we wcześniejszych wersjach jdk wygląda na to, że wyjątek nie jest wychwytywany w pierwszym miejscu, stąd 4, 50 itd.
Teraz, jeśli usuniesz blok try catch w następujący sposób
Wtedy zobaczysz wszystkie wartości
cnt
ant wyrzuconych wyjątków (w jdk 1.7).Użyłem netbeans, aby zobaczyć dane wyjściowe, ponieważ cmd nie pokaże wszystkich danych wyjściowych i zgłoszonych wyjątków.
źródło