Diagnosi e risoluzione di StackOverflowError

Un recente messaggio del forum della comunità JavaWorld (Stack Overflow dopo l'istanza di un nuovo oggetto) mi ha ricordato che le basi di StackOverflowError non sono sempre comprese bene dalle persone che non conoscono Java. Fortunatamente, StackOverflowError è uno degli errori di runtime più facili da eseguire il debug e in questo post sul blog dimostrerò quanto sia facile diagnosticare uno StackOverflowError. Si noti che il potenziale di overflow dello stack non è limitato a Java.

La diagnosi della causa di StackOverflowError può essere abbastanza semplice se il codice è stato compilato con l'opzione di debug attivata in modo che i numeri di riga siano disponibili nella traccia dello stack risultante. In tali casi, è tipicamente semplicemente una questione di trovare lo schema ripetuto dei numeri di riga nella traccia dello stack. Il modello di ripetizione dei numeri di riga è utile perché un StackOverflowError è spesso causato da una ricorsione non terminata. I numeri di riga ripetuti indicano il codice che viene chiamato direttamente o indirettamente in modo ricorsivo. Nota che ci sono situazioni diverse dalla ricorsione illimitata in cui potrebbe verificarsi un overflow dello stack, ma questo post sul blog è limitato a StackOverflowErrorcausa della ricorsione illimitata.

La relazione di ricorsione andata male a StackOverflowErrorè annotata nella descrizione Javadoc per StackOverflowError che afferma che questo errore viene "generato quando si verifica un overflow dello stack perché un'applicazione ricorre troppo in profondità". È significativo che termini StackOverflowErrorcon la parola Error ed è un Error (estende java.lang.Error tramite java.lang.VirtualMachineError) piuttosto che un'eccezione controllata o runtime. La differenza è significativa. I Errore Exceptionsono ciascuno un lanciabile specializzato, ma il loro utilizzo previsto è abbastanza diverso. Il Tutorial Java sottolinea che gli errori sono tipicamente esterni all'applicazione Java e quindi normalmente non possono e non devono essere rilevati o gestiti dall'applicazione.

Dimostrerò di imbattersi in una StackOverflowErrorricorsione illimitata con tre diversi esempi. Il codice utilizzato per questi esempi è contenuto in tre classi, la prima delle quali (e la classe principale) viene mostrata di seguito. Elenchiamo tutte e tre le classi nella loro interezza perché i numeri di riga sono significativi durante il debug di 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 + "]"); } } } 

La classe sopra mostra tre tipi di ricorsione illimitata: ricorsione accidentale e completamente non intenzionale, ricorsione non intenzionale associata a relazioni cicliche intenzionalmente e ricorsione intenzionale con condizione di terminazione insufficiente. Ciascuno di questi e il loro output verranno discussi di seguito.

Ricorsione completamente involontaria

Ci possono essere momenti in cui la ricorsione si verifica senza alcun intento. Una causa comune potrebbe essere la chiamata accidentale di un metodo. Ad esempio, non è troppo difficile essere un po 'troppo disattenti e selezionare la prima raccomandazione di un IDE su un valore di ritorno per un metodo "get" che potrebbe finire per essere una chiamata a quello stesso metodo! Questo è infatti l'esempio mostrato nella classe sopra. Il getStringVar()metodo chiama ripetutamente se stesso finché non StackOverflowErrorviene rilevato. L'output apparirà come segue:

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 

La traccia dello stack mostrata sopra è in realtà molte volte più lunga di quella che ho posizionato sopra, ma è semplicemente lo stesso schema ripetuto. Poiché lo schema si ripete, è facile diagnosticare che la riga 34 della classe è la causa del problema. Quando guardiamo quella linea, vediamo che è davvero l'affermazione return getStringVar()che finisce per chiamare ripetutamente se stessa. In questo caso, possiamo rapidamente renderci conto che il comportamento previsto era invece quello return this.stringVar;.

Ricorsione non intenzionale con relazioni cicliche

Ci sono alcuni rischi nell'avere relazioni cicliche tra le classi. Uno di questi rischi è la maggiore probabilità di incorrere in una ricorsione non intenzionale in cui le dipendenze cicliche vengono continuamente chiamate tra gli oggetti finché lo stack non è in overflow. Per dimostrarlo, utilizzo altre due classi. La Stateclasse e la Cityclasse hanno una relazione ciclica perché Stateun'istanza ha un riferimento alla sua capitale Citye a Cityha un riferimento al Statein cui si trova.

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