StackOverflowError diagnostizieren und beheben

Eine aktuelle JavaWorld Community-Forumsnachricht (Stapelüberlauf nach Instanziierung eines neuen Objekts) erinnerte mich daran, dass die Grundlagen des StackOverflowError von Java-Neulingen nicht immer gut verstanden werden. Glücklicherweise ist der StackOverflowError einer der am einfachsten zu debuggenden Laufzeitfehler. In diesem Blogbeitrag werde ich zeigen, wie einfach es ist, einen StackOverflowError häufig zu diagnostizieren. Beachten Sie, dass das Potenzial für einen Stapelüberlauf nicht auf Java beschränkt ist.

Die Diagnose der Ursache eines StackOverflowError kann recht einfach sein, wenn der Code mit aktivierter Debug-Option kompiliert wurde, sodass Zeilennummern im resultierenden Stack-Trace verfügbar sind. In solchen Fällen geht es normalerweise einfach darum, das sich wiederholende Muster von Zeilennummern in der Stapelspur zu finden. Das Muster der Wiederholung von Zeilennummern ist hilfreich, da ein StackOverflowError häufig durch eine nicht abgeschlossene Rekursion verursacht wird. Die sich wiederholenden Zeilennummern geben den Code an, der direkt oder indirekt rekursiv aufgerufen wird. Beachten Sie, dass es andere Situationen als eine unbegrenzte Rekursion gibt, in denen ein Stapelüberlauf auftreten kann. Dieser Blogeintrag ist jedoch auf StackOverflowErroreine unbegrenzte Rekursion beschränkt.

In StackOverflowErrorder Javadoc-Beschreibung für StackOverflowError wird angegeben, dass dieser Fehler "Wird ausgelöst, wenn ein Stapelüberlauf auftritt, weil eine Anwendung zu tief rekursiv ist". Es ist wichtig, dass es StackOverflowErrormit dem Wort Fehler endet und eher ein Fehler ist (erweitert java.lang.Error über java.lang.VirtualMachineError) als eine überprüfte oder Laufzeitausnahme. Der Unterschied ist signifikant. Die Errorund Exceptionsind jeweils ein spezialisiertes Throwable, aber ihre beabsichtigte Handhabung ist ganz anders. Das Java-Lernprogramm weist darauf hin, dass Fehler normalerweise außerhalb der Java-Anwendung liegen und daher normalerweise nicht von der Anwendung abgefangen oder behandelt werden können und sollten.

Ich werde anhand von StackOverflowErrordrei verschiedenen Beispielen demonstrieren, wie man durch unbegrenzte Rekursion auf sie stößt. Der für diese Beispiele verwendete Code ist in drei Klassen enthalten, von denen die erste (und die Hauptklasse) als nächstes angezeigt wird. Ich liste alle drei Klassen in ihrer Gesamtheit auf, da Zeilennummern beim Debuggen von wichtig sind StackOverflowError.

StackOverflowErrorDemonstrator.java

package dustin.examples.stackoverflow; import java.io.IOException; import java.io.OutputStream; /** * This class demonstrates different ways that a StackOverflowError might * occur. */ public class StackOverflowErrorDemonstrator { private static final String NEW_LINE = System.getProperty("line.separator"); /** Arbitrary String-based data member. */ private String stringVar = ""; /** * Simple accessor that will shown unintentional recursion gone bad. Once * invoked, this method will repeatedly call itself. Because there is no * specified termination condition to terminate the recursion, a * StackOverflowError is to be expected. * * @return String variable. */ public String getStringVar() { // // WARNING: // // This is BAD! This will recursively call itself until the stack // overflows and a StackOverflowError is thrown. The intended line in // this case should have been: // return this.stringVar; return getStringVar(); } /** * Calculate factorial of the provided integer. This method relies upon * recursion. * * @param number The number whose factorial is desired. * @return The factorial value of the provided number. */ public int calculateFactorial(final int number) { // WARNING: This will end badly if a number less than zero is provided. // A better way to do this is shown here, but commented out. //return number <= 1 ? 1 : number * calculateFactorial(number-1); return number == 1 ? 1 : number * calculateFactorial(number-1); } /** * This method demonstrates how unintended recursion often leads to * StackOverflowError because no termination condition is provided for the * unintended recursion. */ public void runUnintentionalRecursionExample() { final String unusedString = this.getStringVar(); } /** * This method demonstrates how unintended recursion as part of a cyclic * dependency can lead to StackOverflowError if not carefully respected. */ public void runUnintentionalCyclicRecusionExample() { final State newMexico = State.buildState("New Mexico", "NM", "Santa Fe"); System.out.println("The newly constructed State is:"); System.out.println(newMexico); } /** * Demonstrates how even intended recursion can result in a StackOverflowError * when the terminating condition of the recursive functionality is never * satisfied. */ public void runIntentionalRecursiveWithDysfunctionalTermination() { final int numberForFactorial = -1; System.out.print("The factorial of " + numberForFactorial + " is: "); System.out.println(calculateFactorial(numberForFactorial)); } /** * Write this class's main options to the provided OutputStream. * * @param out OutputStream to which to write this test application's options. */ public static void writeOptionsToStream(final OutputStream out) { final String option1 = "1. Unintentional (no termination condition) single method recursion"; final String option2 = "2. Unintentional (no termination condition) cyclic recursion"; final String option3 = "3. Flawed termination recursion"; try { out.write((option1 + NEW_LINE).getBytes()); out.write((option2 + NEW_LINE).getBytes()); out.write((option3 + NEW_LINE).getBytes()); } catch (IOException ioEx) { System.err.println("(Unable to write to provided OutputStream)"); System.out.println(option1); System.out.println(option2); System.out.println(option3); } } /** * Main function for running StackOverflowErrorDemonstrator. */ public static void main(final String[] arguments) { if (arguments.length < 1) { System.err.println( "You must provide an argument and that single argument should be"); System.err.println( "one of the following options:"); writeOptionsToStream(System.err); System.exit(-1); } int option = 0; try { option = Integer.valueOf(arguments[0]); } catch (NumberFormatException notNumericFormat) { System.err.println( "You entered an non-numeric (invalid) option [" + arguments[0] + "]"); writeOptionsToStream(System.err); System.exit(-2); } final StackOverflowErrorDemonstrator me = new StackOverflowErrorDemonstrator(); switch (option) { case 1 : me.runUnintentionalRecursionExample(); break; case 2 : me.runUnintentionalCyclicRecusionExample(); break; case 3 : me.runIntentionalRecursiveWithDysfunctionalTermination(); break; default : System.err.println("You provided an unexpected option [" + option + "]"); } } } 

Die obige Klasse zeigt drei Arten von unbegrenzter Rekursion: versehentliche und völlig unbeabsichtigte Rekursion, unbeabsichtigte Rekursion in Verbindung mit absichtlich zyklischen Beziehungen und beabsichtigte Rekursion mit unzureichender Beendigungsbedingung. Jedes von diesen und ihre Ausgabe werden als nächstes diskutiert.

Völlig unbeabsichtigte Rekursion

Es kann Zeiten geben, in denen eine Rekursion ohne jegliche Absicht auftritt. Eine häufige Ursache könnte sein, dass sich eine Methode versehentlich selbst aufruft. Zum Beispiel ist es nicht allzu schwierig, ein wenig zu nachlässig zu werden und die erste Empfehlung einer IDE für einen Rückgabewert für eine "get" -Methode auszuwählen, die möglicherweise einen Aufruf derselben Methode darstellt! Dies ist in der Tat das in der obigen Klasse gezeigte Beispiel. Die getStringVar()Methode ruft sich wiederholt auf, bis die gefunden StackOverflowErrorwird. Die Ausgabe sieht wie folgt aus:

Exception in thread "main" java.lang.StackOverflowError at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at 

Die oben gezeigte Stapelspur ist tatsächlich um ein Vielfaches länger als die, die ich oben platziert habe, aber es ist einfach das gleiche sich wiederholende Muster. Da sich das Muster wiederholt, ist es leicht zu diagnostizieren, dass Zeile 34 der Klasse der Problemverursacher ist. Wenn wir uns diese Zeile ansehen, sehen wir, dass es tatsächlich die Aussage ist return getStringVar(), die sich wiederholt selbst aufruft. In diesem Fall können wir schnell erkennen, dass das beabsichtigte Verhalten stattdessen war return this.stringVar;.

Unbeabsichtigte Rekursion mit zyklischen Beziehungen

Es gibt bestimmte Risiken für zyklische Beziehungen zwischen Klassen. Eines dieser Risiken ist die größere Wahrscheinlichkeit einer unbeabsichtigten Rekursion, bei der die zyklischen Abhängigkeiten zwischen Objekten kontinuierlich aufgerufen werden, bis der Stapel überläuft. Um dies zu demonstrieren, benutze ich zwei weitere Klassen. Die StateKlasse und die CityKlasse haben eine zyklische Beziehung, weil eine StateInstanz einen Verweis auf ihr Kapital hat Cityund a Cityeinen Verweis auf das, Statein dem sie sich befindet.

State.java

package dustin.examples.stackoverflow; /** * A class that represents a state and is intentionally part of a cyclic * relationship between City and State. */ public class State { private static final String NEW_LINE = System.getProperty("line.separator"); /** Name of the state. */ private String name; /** Two-letter abbreviation for state. */ private String abbreviation; /** City that is the Capital of the State. */ private City capitalCity; /** * Static builder method that is the intended method for instantiation of me. * * @param newName Name of newly instantiated State. * @param newAbbreviation Two-letter abbreviation of State. * @param newCapitalCityName Name of capital city. */ public static State buildState( final String newName, final String newAbbreviation, final String newCapitalCityName) { final State instance = new State(newName, newAbbreviation); instance.capitalCity = new City(newCapitalCityName, instance); return instance; } /** * Parameterized constructor accepting data to populate new instance of State. * * @param newName Name of newly instantiated State. * @param newAbbreviation Two-letter abbreviation of State. */ private State( final String newName, final String newAbbreviation) { this.name = newName; this.abbreviation = newAbbreviation; } /** * Provide String representation of the State instance. * * @return My String representation. */ @Override public String toString() { // WARNING: This will end badly because it calls City's toString() // method implicitly and City's toString() method calls this // State.toString() method. return "StateName: " + this.name + NEW_LINE + "StateAbbreviation: " + this.abbreviation + NEW_LINE + "CapitalCity: " + this.capitalCity; } } 

City.java