Warum druckt diese Methode 4?

111

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, StackOverflowErrorbevor etwas gedruckt wird), sondern warum es den bestimmten Wert 4 bzw. 0,3,8,55 oder etwas anderes auf einem anderen hat Systeme.

flrnb
quelle
4
In meiner Region erhalte ich erwartungsgemäß "0".
Reddy
2
Dies kann mit vielen Architektur-Sachen zu tun haben. Also poste deine Ausgabe besser mit der JDK-Version. Für mich ist die Ausgabe 0 auf JDK 1.7
Lokesh
3
Ich habe bekommen 5, 6und 38mit Java 1.7.0_10
Kon
8
@Elist Wird nicht die gleiche Ausgabe sein, wenn Sie Tricks mit zugrunde liegender Architektur machen;)
m0skit0
3
@flrnb Es ist nur ein Stil, mit dem ich die Klammern ausrichte. Es erleichtert mir zu wissen, wo Bedingungen und Funktionen beginnen und enden. Sie können es ändern, wenn Sie möchten, aber meiner Meinung nach ist es auf diese Weise besser lesbar.
Syb0rg

Antworten:

41

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

  • X ist die Gesamtstapelgröße
  • M ist der Stapelplatz, der verwendet wird, wenn wir das erste Mal main eingeben
  • R ist die Erhöhung des Stapelplatzes bei jedem Eintritt in main
  • P ist der zum Ausführen erforderliche Stapelspeicherplatz 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 printlnes 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

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

Dann ist C = Boden ((XM) / R) = 49 und cnt = Decke ((P - (X - M - C * R)) / R) = 0.

Beispiel 2: Angenommen, das

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

Dann ist C = 19 und cnt = 2.

Beispiel 3: Angenommen, das

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

Dann ist C = 20 und cnt = 3.

Beispiel 4: Angenommen, das

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

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 catchzum Starten benötigt wird. Solange nicht genügend Platz vorhanden ist catch, 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, printlnwenn 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

John Tseng
quelle
Vielen Dank für die gute Zusammenfassung. Ich denke, alles läuft auf die Frage hinaus: Welche Auswirkungen haben M, R und P (da X mit der VM-Option -Xss festgelegt werden kann)?
Flrnb
@flrnb M, R und P sind systemspezifisch. Sie können diese nicht einfach ändern. Ich erwarte, dass sie sich auch zwischen einigen Veröffentlichungen unterscheiden.
John Tseng
Warum erhalte ich dann unterschiedliche Ergebnisse, wenn ich Xss (auch bekannt als X) ändere? Das Ändern von X von 100 auf 10000 sollte, da M, R und P gleich bleiben, cnt gemäß Ihrer Formel nicht beeinflussen, oder irre ich mich?
Flrnb
@flrnb X allein ändert cnt aufgrund der diskreten Natur dieser Variablen. Die Beispiele 2 und 3 unterscheiden sich nur in X, aber cnt ist unterschiedlich.
John Tseng
1
@JohnTseng Ich halte Ihre Antwort auch für die verständlichste und vollständigste - jedenfalls würde mich wirklich interessieren, wie der Stapel tatsächlich aussieht, wenn er StackOverflowErrorgeworfen 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.
Flrnb
20

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.

java -Xss1024k RandomNumberGenerator

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:

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

        }        
    }        
}

AKTUALISIEREN:

Da dies viel mehr Aufmerksamkeit erhält, wollen wir ein weiteres Beispiel geben, um die Dinge klarer zu machen.

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

}

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.

Sajal Dutta
quelle
13

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 :

// Die Standardstapelgröße unter Windows wird von der ausführbaren Datei bestimmt (java.exe
// hat einen Standardwert von 320 KB / 1 MB [32 Bit / 64 Bit]). Abhängig von der Windows-Version kann das Ändern von
// ThreadStackSize auf einen Wert ungleich Null erhebliche Auswirkungen auf die Speichernutzung haben.
// Siehe Kommentare in os_windows.cpp.

Wenn das StackOverflowErrorausgelöst wird, wird der Fehler im catch-Block abgefangen. Hier println()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 javaAnrufen mit -Xss 4Mgibt mir 41. Daher die Korrelation.

Jatin
quelle
4
Ich verstehe nicht, warum sich die Stapelgröße auf das Ergebnis auswirken sollte, da sie bereits überschritten wird, wenn wir versuchen, den Wert von cnt zu drucken. Der einzige Unterschied könnte also in der "Stapelgröße jedes Funktionsaufrufs" liegen. Und ich verstehe nicht, warum dies zwischen zwei Computern variieren sollte, auf denen dieselbe JVM-Version ausgeführt wird.
Flrnb
Das genaue Verhalten kann nur von der JVM-Quelle abgerufen werden. Aber der Grund könnte dies sein. Denken Sie daran, dass gerade catchein 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 von catchund so hinzu. Dies könnte das Verhalten sein. Dies ist nur Spekulation.
Jatin
Die Stapelgröße kann bei zwei verschiedenen Maschinen unterschiedlich sein. Die Stapelgröße hängt von vielen os-basierten Faktoren ab, nämlich der Größe der Speicherseite usw.
Jatin
6

Ich denke, die angezeigte Nummer gibt an, wie oft der System.out.printlnAnruf die StackoverflowAusnahme auslöst .

Dies hängt wahrscheinlich von der Implementierung des printlnund der Anzahl der darin enthaltenen Stapelaufrufe ab.

Als Illustration:

Der main()Anruf löst die StackoverflowAusnahme beim Aufruf i aus. Der i-1-Aufruf von main fängt die Ausnahme und den Aufruf ab, printlndie eine Sekunde auslösen Stackoverflow. cntInkrement auf 1 setzen. Der i-2-Aufruf von main catch jetzt die Ausnahme und call println. In printlneiner Methode wird das Auslösen einer dritten Ausnahme aufgerufen. cntInkrement auf 2 setzen. Dies wird fortgesetzt, bis printlnalle erforderlichen Aufrufe getätigt und schließlich der Wert von angezeigt werden können cnt.

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 printlnImplementierung keine Aufrufe durch, oder die ++ - Operation wird danach ausgeführt Der printlnAnruf wird also von der Ausnahme umgangen.

Kazaag
quelle
Das habe ich mit "Ich dachte, vielleicht lag es daran, dass System.out.println 3 Segmente auf dem Aufrufstapel benötigt" gemeint - aber ich war verwirrt, warum es genau diese Nummer ist, und jetzt bin ich noch verwirrter, warum sich die Nummer zwischen den verschiedenen so stark unterscheidet (virtuelle) Maschinen
flrnb
Ich stimme dem teilweise zu, aber was ich nicht zustimme, ist die Aussage "abhängig von der tatsächlichen Implementierung von println". Sie hat eher mit der Stapelgröße in jedem JVM als mit der Implementierung zu tun.
Jatin
6
  1. mainrekursiv auf sich selbst, bis es den Stapel in Rekursionstiefe überläuft R.
  2. Der Fangblock in Rekursionstiefe R-1wird ausgeführt.
  3. Der Fangblock bei Rekursionstiefe wird R-1ausgewertet cnt++.
  4. Der Catch-Block in der Tiefe R-1ruft auf printlnund platziert cntden alten Wert auf dem Stapel. printlnruft intern andere Methoden auf und verwendet lokale Variablen und Dinge. Alle diese Prozesse erfordern Stapelspeicher.
  5. Da der Stapel bereits das Limit überschritten hat und das Aufrufen / Ausführen printlnStapelspeicherplatz erfordert, wird ein neuer Stapelüberlauf in der Tiefe R-1anstelle der Tiefe ausgelöst R.
  6. Die Schritte 2 bis 5 werden erneut ausgeführt, jedoch in Rekursionstiefe R-2.
  7. Die Schritte 2 bis 5 werden erneut ausgeführt, jedoch in Rekursionstiefe R-3.
  8. Die Schritte 2 bis 5 werden erneut ausgeführt, jedoch in Rekursionstiefe R-4.
  9. Die Schritte 2 bis 4 werden erneut ausgeführt, jedoch in Rekursionstiefe R-5.
  10. Es kommt also vor, dass jetzt genügend Stapelspeicher vorhanden ist, um den Vorgang printlnabzuschließen (beachten Sie, dass dies ein Implementierungsdetail ist, das variieren kann).
  11. cntin Tiefen wurde nachinkrementiert R-1, R-2, R-3, R-4, und schließlich an R-5. Das fünfte Post-Inkrement ergab vier, was gedruckt wurde.
  12. Mit mainerfolgreich in der Tiefe abgeschlossen R-5, wobei die gesamte Stapel abwickelt , ohne mehr catch - Blöcke laufen und das Programm wird beendet.
Craig Gidney
quelle
1

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 StackOverflowErrorWurf 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:

Wenn nicht genügend Speicher verfügbar ist, um einen solchen Aktivierungsrahmen zu erstellen, wird ein StackOverflowError ausgelöst.

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 ,

Frames können Heap zugewiesen werden.

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. Wenn StackOverflowErrores 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 benutzt cnt++. Ä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.printlnetwa das 51-fache main, daher geht sie 51-mal zurück und druckt 50.

Jay
quelle
Mein erster Gedanke war auch, die Ebenen der Methodenaufrufe zu zählen (und Sie haben Recht, ich habe nicht darauf geachtet, dass ich Inkrement-cnt poste), aber wenn die Lösung so einfach wäre, warum würden die Ergebnisse plattformübergreifend so stark variieren und VM-Implementierungen?
Flrnb
@flrnb Das liegt daran, dass unterschiedliche Plattformen die Größe des Stapelrahmens beeinflussen können und unterschiedliche Versionen von jre die Implementierung System.out.printoder die Strategie zur Methodenausführung beeinflussen. Wie oben beschrieben, wirkt sich die VM-Implementierung auch darauf aus, wo der Stapelrahmen tatsächlich gespeichert wird.
Jay
0

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

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

Dann sehen Sie alle Werte von cntant 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.

me_digvijay
quelle