Ich habe mich gefragt, was passiert, wenn Sie versuchen, einen StackOverflowError abzufangen, und die folgende Methode gefunden hat:
class RandomNumberGenerator {
static int cnt = 0;
public static void main(String[] args) {
try {
main(args);
} catch (StackOverflowError ignore) {
System.out.println(cnt++);
}
}
}
Nun meine Frage:
Warum druckt diese Methode '4'?
Ich dachte, vielleicht lag es daran, dass System.out.println()
3 Segmente auf dem Aufrufstapel benötigt werden, aber ich weiß nicht, woher die Nummer 3 kommt. Wenn Sie sich den Quellcode (und den Bytecode) von ansehen System.out.println()
, führt dies normalerweise zu weitaus mehr Methodenaufrufen als 3 (3 Segmente auf dem Aufrufstapel wären also nicht ausreichend). Wenn es an Optimierungen liegt, die die Hotspot-VM anwendet (Methoden-Inlining), frage ich mich, ob das Ergebnis auf einer anderen VM anders wäre.
Bearbeiten :
Da die Ausgabe sehr JVM-spezifisch zu sein scheint, erhalte ich das Ergebnis 4 unter Verwendung der
Java (TM) SE-Laufzeitumgebung (Build 1.6.0_41-b02)
Java HotSpot (TM) 64-Bit-Server-VM (Build 20.14-b01, gemischter Modus).
Erklärung, warum sich diese Frage meiner Meinung nach vom Verständnis des Java-Stacks unterscheidet :
Meine Frage ist nicht, warum es ein cnt> 0 gibt (offensichtlich, weil es eine Stapelgröße System.out.println()
erfordert und ein anderes wirft, StackOverflowError
bevor etwas gedruckt wird), sondern warum es den bestimmten Wert 4 bzw. 0,3,8,55 oder etwas anderes auf einem anderen hat Systeme.
quelle
5
,6
und38
mit Java 1.7.0_10Antworten:
Ich denke, die anderen haben gute Arbeit geleistet, um zu erklären, warum cnt> 0 ist, aber es gibt nicht genügend Details darüber, warum cnt = 4 ist und warum cnt in den verschiedenen Einstellungen so stark variiert. Ich werde versuchen, diese Lücke hier zu füllen.
Lassen
System.out.println
Wenn wir zum ersten Mal in die Hauptleitung kommen, bleibt nur noch XM übrig. Jeder rekursive Aufruf beansprucht R mehr Speicher. Für 1 rekursiven Aufruf (1 mehr als das Original) ist die Speichernutzung M + R. Angenommen, StackOverflowError wird nach C erfolgreichen rekursiven Aufrufen ausgelöst, dh M + C * R <= X und M + C * (R +) 1)> X. Zum Zeitpunkt des ersten StackOverflowError ist noch X-M-C * R-Speicher vorhanden.
Um laufen zu können
System.out.prinln
, benötigen wir P verbleibenden Speicherplatz auf dem Stapel. Wenn es so kommt, dass X - M - C * R> = P ist, wird 0 gedruckt. Wenn P mehr Speicherplatz benötigt, entfernen wir Frames vom Stapel und gewinnen R-Speicher auf Kosten von cnt ++.Wenn
println
es endlich laufen kann, ist X - M - (C - cnt) * R> = P. Wenn also P für ein bestimmtes System groß ist, ist cnt groß.Schauen wir uns dies anhand einiger Beispiele an.
Beispiel 1: Angenommen
Dann ist C = Boden ((XM) / R) = 49 und cnt = Decke ((P - (X - M - C * R)) / R) = 0.
Beispiel 2: Angenommen, das
Dann ist C = 19 und cnt = 2.
Beispiel 3: Angenommen, das
Dann ist C = 20 und cnt = 3.
Beispiel 4: Angenommen, das
Dann ist C = 19 und cnt = 2.
Wir sehen also, dass sowohl das System (M, R und P) als auch die Stapelgröße (X) cnt beeinflussen.
Nebenbei bemerkt spielt es keine Rolle, wie viel Platz
catch
zum Starten benötigt wird. Solange nicht genügend Platz vorhanden istcatch
, wird cnt nicht erhöht, sodass keine externen Effekte auftreten.BEARBEITEN
Ich nehme zurück, was ich gesagt habe
catch
. Es spielt eine Rolle. Angenommen, zum Starten wird T Speicherplatz benötigt. cnt beginnt zu erhöhen, wenn der verbleibende Raum größer als T ist, und wird ausgeführt,println
wenn der verbleibende Raum größer als T + P ist. Dies fügt den Berechnungen einen zusätzlichen Schritt hinzu und trübt die bereits schlammige Analyse weiter.BEARBEITEN
Ich fand endlich Zeit, einige Experimente durchzuführen, um meine Theorie zu stützen. Leider scheint die Theorie nicht mit den Experimenten übereinzustimmen. Was tatsächlich passiert, ist ganz anders.
Versuchsaufbau: Ubuntu 12.04 Server mit Standard Java und Standard-JDK. Xss ab 70.000 in Schritten von 1 Byte bis 460.000.
Die Ergebnisse sind verfügbar unter: https://www.google.com/fusiontables/DataSource?docid=1xkJhd4s8biLghe6gZbcfUs3vT5MpS_OnscjWDbM Ich habe eine andere Version erstellt, in der jeder wiederholte Datenpunkt entfernt wird. Mit anderen Worten, es werden nur Punkte angezeigt, die sich von den vorherigen unterscheiden. Dies erleichtert das Erkennen von Anomalien. https://www.google.com/fusiontables/DataSource?docid=1XG_SRzrrNasepwZoNHqEAKuZlHiAm9vbEdwfsUA
quelle
StackOverflowError
geworfen wird und wie sich dies auf die Ausgabe auswirkt. Wenn es nur einen Verweis auf einen Stapelrahmen auf dem Heap enthält (wie Jay vorgeschlagen hat), sollte die Ausgabe für ein bestimmtes System ziemlich vorhersehbar sein.Dies ist das Opfer eines schlechten rekursiven Aufrufs. Wenn Sie sich fragen, warum der Wert von cnt variiert, liegt dies daran, dass die Stapelgröße von der Plattform abhängt. Java SE 6 unter Windows hat eine Standardstapelgröße von 320 KB in der 32-Bit-VM und 1024 KB in der 64-Bit-VM. Sie können mehr lesen hier .
Sie können mit verschiedenen Stapelgrößen ausgeführt werden und sehen unterschiedliche Werte von cnt, bevor der Stapel überläuft.
Der Wert von cnt wird nicht mehrmals gedruckt, obwohl der Wert manchmal größer als 1 ist, da Ihre Druckanweisung auch einen Fehler auslöst, den Sie debuggen können, um sicherzugehen, dass er über Eclipse oder andere IDEs erfolgt.
Sie können den Code wie folgt ändern, um pro Anweisungsausführung zu debuggen, wenn Sie möchten:
AKTUALISIEREN:
Da dies viel mehr Aufmerksamkeit erhält, wollen wir ein weiteres Beispiel geben, um die Dinge klarer zu machen.
Wir haben eine andere Methode namens overflow erstellt , um eine fehlerhafte Rekursion durchzuführen, und die println- Anweisung aus dem catch-Block entfernt, damit beim Drucken keine weiteren Fehler ausgegeben werden . Dies funktioniert wie erwartet. Sie können versuchen, System.out.println (cnt) zu setzen. Anweisung nach cnt ++ oben und kompilieren. Führen Sie dann mehrere Male aus. Abhängig von Ihrer Plattform erhalten Sie möglicherweise unterschiedliche Werte für cnt .
Aus diesem Grund fangen wir im Allgemeinen keine Fehler ab, da Rätsel im Code keine Fantasie sind.
quelle
Das Verhalten hängt von der Stapelgröße ab (die manuell festgelegt werden kann
Xss
. Die Stapelgröße ist architekturspezifisch. Aus dem JDK 7- Quellcode :Wenn das
StackOverflowError
ausgelöst wird, wird der Fehler im catch-Block abgefangen. Hierprintln()
ist ein weiterer Stapelaufruf, der erneut eine Ausnahme auslöst. Dies wird wiederholt.Wie oft wiederholt es sich? - Nun, es hängt davon ab, wann JVM denkt, dass es kein Stapelüberlauf mehr ist. Und das hängt von der Stapelgröße jedes Funktionsaufrufs (schwer zu finden) und der
Xss
. Wie oben erwähnt, ist die Standardgesamtgröße und -größe jedes Funktionsaufrufs (abhängig von der Größe der Speicherseite usw.) plattformspezifisch. Daher anderes Verhalten.Das
java
Anrufen mit-Xss 4M
gibt mir41
. Daher die Korrelation.quelle
catch
ein Block ist, der Speicher auf dem Stapel belegt. Wie viel Speicher jeder Methodenaufruf benötigt, ist nicht bekannt. Wenn der Stapel gelöscht wird, fügen Sie einen weiteren Block voncatch
und so hinzu. Dies könnte das Verhalten sein. Dies ist nur Spekulation.Ich denke, die angezeigte Nummer gibt an, wie oft der
System.out.println
Anruf dieStackoverflow
Ausnahme auslöst .Dies hängt wahrscheinlich von der Implementierung des
println
und der Anzahl der darin enthaltenen Stapelaufrufe ab.Als Illustration:
Der
main()
Anruf löst dieStackoverflow
Ausnahme beim Aufruf i aus. Der i-1-Aufruf von main fängt die Ausnahme und den Aufruf ab,println
die eine Sekunde auslösenStackoverflow
.cnt
Inkrement auf 1 setzen. Der i-2-Aufruf von main catch jetzt die Ausnahme und callprintln
. Inprintln
einer Methode wird das Auslösen einer dritten Ausnahme aufgerufen.cnt
Inkrement auf 2 setzen. Dies wird fortgesetzt, bisprintln
alle erforderlichen Aufrufe getätigt und schließlich der Wert von angezeigt werden könnencnt
.Dies ist dann abhängig von der tatsächlichen Implementierung von
println
.Für das JDK7 erkennt es entweder einen zyklischen Aufruf und löst die Ausnahme früher aus, oder es behält eine Stapelressource bei und löst die Ausnahme aus, bevor das Limit erreicht wird, um Platz für die Korrekturlogik zu schaffen. Entweder führt die
println
Implementierung keine Aufrufe durch, oder die ++ - Operation wird danach ausgeführt Derprintln
Anruf wird also von der Ausnahme umgangen.quelle
main
rekursiv auf sich selbst, bis es den Stapel in Rekursionstiefe überläuftR
.R-1
wird ausgeführt.R-1
ausgewertetcnt++
.R-1
ruft aufprintln
und platziertcnt
den alten Wert auf dem Stapel.println
ruft intern andere Methoden auf und verwendet lokale Variablen und Dinge. Alle diese Prozesse erfordern Stapelspeicher.println
Stapelspeicherplatz erfordert, wird ein neuer Stapelüberlauf in der TiefeR-1
anstelle der Tiefe ausgelöstR
.R-2
.R-3
.R-4
.R-5
.println
abzuschließen (beachten Sie, dass dies ein Implementierungsdetail ist, das variieren kann).cnt
in Tiefen wurde nachinkrementiertR-1
,R-2
,R-3
,R-4
, und schließlich anR-5
. Das fünfte Post-Inkrement ergab vier, was gedruckt wurde.main
erfolgreich in der Tiefe abgeschlossenR-5
, wobei die gesamte Stapel abwickelt , ohne mehr catch - Blöcke laufen und das Programm wird beendet.quelle
Nachdem ich eine Weile herumgegraben habe, kann ich nicht sagen, dass ich die Antwort finde, aber ich denke, es ist jetzt ziemlich nah.
Zuerst müssen wir wissen, wann ein
StackOverflowError
Wurf geworfen wird. Tatsächlich speichert der Stapel für einen Java-Thread Frames, die alle Daten enthalten, die zum Aufrufen einer Methode und zum Fortsetzen erforderlich sind. Gemäß den Java-Sprachspezifikationen für JAVA 6 wird beim Aufrufen einer Methode Folgendes ausgeführt:Zweitens sollten wir klarstellen, was " nicht genügend Speicher verfügbar ist, um einen solchen Aktivierungsrahmen zu erstellen ". Nach Java Virtual Machine Spezifikationen für Java 6 ,
Wenn also ein Frame erstellt wird, sollte genügend Heap-Speicherplatz zum Erstellen eines Stack-Frames und genügend Stack-Speicherplatz zum Speichern der neuen Referenz vorhanden sein, die auf den neuen Stack-Frame verweist, wenn dem Frame Heap zugewiesen ist.
Kommen wir nun zur Frage zurück. Aus dem oben Gesagten können wir erkennen, dass eine ausgeführte Methode möglicherweise nur dieselbe Menge an Stapelspeicher kostet. Und das Aufrufen
System.out.println
(möglicherweise) erfordert 5 Methodenaufrufebenen, sodass 5 Frames erstellt werden müssen. WennStackOverflowError
es dann weggeworfen wird, muss es 5 Mal zurückgehen, um genügend Stapelspeicherplatz zum Speichern der Referenzen von 5 Frames zu erhalten. Daher wird 4 ausgedruckt. Warum nicht 5? Weil du benutztcnt++
. Ändern Sie es in++cnt
, und dann erhalten Sie 5.Und Sie werden feststellen, dass Sie manchmal 50 erhalten, wenn die Größe des Stapels auf ein hohes Niveau steigt. Dies liegt daran, dass die Menge des verfügbaren Heapspeichers dann berücksichtigt werden muss. Wenn der Stapel zu groß ist, wird möglicherweise vor dem Stapel der Heap-Speicherplatz knapp. Und (vielleicht) beträgt die tatsächliche Größe der Stapelrahmen
System.out.println
etwa das 51-fachemain
, daher geht sie 51-mal zurück und druckt 50.quelle
System.out.print
oder die Strategie zur Methodenausführung beeinflussen. Wie oben beschrieben, wirkt sich die VM-Implementierung auch darauf aus, wo der Stapelrahmen tatsächlich gespeichert wird.Dies ist nicht gerade eine Antwort auf die Frage, aber ich wollte nur etwas zu der ursprünglichen Frage hinzufügen, auf die ich gestoßen bin und wie ich das Problem verstanden habe:
Im ursprünglichen Problem wird die Ausnahme dort abgefangen, wo es möglich war:
Zum Beispiel wird es mit jdk 1.7 am ersten Ort des Auftretens abgefangen.
aber in früheren Versionen von jdk sieht es so aus, als würde die Ausnahme nicht am ersten Ort des Auftretens abgefangen, daher 4, 50 usw.
Wenn Sie nun den try catch-Block wie folgt entfernen
Dann sehen Sie alle Werte von
cnt
ant die ausgelösten Ausnahmen (auf jdk 1.7).Ich habe Netbeans verwendet, um die Ausgabe zu sehen, da der Cmd nicht alle Ausgaben und Ausnahmen anzeigt.
quelle