Diagnóstico y resolución de StackOverflowError

Un mensaje reciente del foro de la Comunidad JavaWorld (Stack Overflow después de crear una instancia de un nuevo objeto) me recordó que los conceptos básicos de StackOverflowError no siempre los entienden bien las personas nuevas en Java. Afortunadamente, StackOverflowError es uno de los errores de tiempo de ejecución más fáciles de depurar y en esta publicación de blog demostraré lo fácil que es diagnosticar un StackOverflowError. Tenga en cuenta que el potencial de desbordamiento de pila no se limita a Java.

Diagnosticar la causa de un StackOverflowError puede ser bastante sencillo si el código se ha compilado con la opción de depuración activada para que los números de línea estén disponibles en el seguimiento de la pila resultante. En tales casos, normalmente se trata simplemente de encontrar el patrón repetido de números de línea en el seguimiento de la pila. El patrón de repetición de números de línea es útil porque un StackOverflowError a menudo es causado por una recursividad no terminada. Los números de línea repetidos indican el código que se llama directa o indirectamente de forma recursiva. Tenga en cuenta que hay situaciones distintas de la recursividad ilimitada en las que puede producirse un desbordamiento de pila, pero esta publicación de blog se limita a la StackOverflowErrorcausada por la recursividad ilimitada.

La relación entre la recursividad que salió mal StackOverflowErrorse indica en la descripción de Javadoc para StackOverflowError que indica que este error es "Lanzado cuando se produce un desbordamiento de pila porque una aplicación recurre demasiado profundamente". Es significativo que StackOverflowErrortermine con la palabra Error y sea un Error (extiende java.lang.Error a través de java.lang.VirtualMachineError) en lugar de una Excepción comprobada o en tiempo de ejecución. La diferencia es significativa. Los Errory Exceptionson cada uno un Throwable especializado, pero su manejo previsto es bastante diferente. El Tutorial de Java señala que los errores suelen ser externos a la aplicación Java y, por lo tanto, la aplicación no puede ni debe detectarlos ni manejarlos.

Demostraré el encuentro a StackOverflowErrortravés de la recursividad ilimitada con tres ejemplos diferentes. El código utilizado para estos ejemplos está contenido en tres clases, la primera de las cuales (y la clase principal) se muestra a continuación. Enumero las tres clases en su totalidad porque los números de línea son significativos al depurar el 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 clase anterior demuestra tres tipos de recursividad ilimitada: recursión accidental y completamente involuntaria, recursión no intencionada asociada con relaciones cíclicas intencionalmente y recursión intencionada con condición de terminación insuficiente. Cada uno de estos y sus resultados se analizan a continuación.

Recurrencia completamente involuntaria

Puede haber ocasiones en las que la recursividad se produzca sin intención alguna. Una causa común puede ser que un método se llame a sí mismo accidentalmente. Por ejemplo, no es demasiado difícil ser demasiado descuidado y seleccionar la primera recomendación de un IDE sobre un valor de retorno para un método "get" que podría terminar siendo una llamada a ese mismo método. De hecho, este es el ejemplo que se muestra en la clase anterior. El getStringVar()método se llama repetidamente a sí mismo hasta que StackOverflowErrorse encuentra. La salida aparecerá de la siguiente manera:

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 

El rastro de pila que se muestra arriba en realidad es muchas veces más largo que el que coloqué arriba, pero es simplemente el mismo patrón repetido. Debido a que el patrón se repite, es fácil diagnosticar que la línea 34 de la clase es la causa del problema. Cuando miramos esa línea, vemos que de hecho es la declaración return getStringVar()que termina llamándose a sí misma repetidamente. En este caso, podemos darnos cuenta rápidamente de que el comportamiento previsto era en su lugar return this.stringVar;.

Recursividad involuntaria con relaciones cíclicas

Existen ciertos riesgos de tener relaciones cíclicas entre clases. Uno de estos riesgos es la mayor probabilidad de encontrarse con una recursividad no intencionada donde las dependencias cíclicas se llaman continuamente entre objetos hasta que la pila se desborda. Para demostrar esto, utilizo dos clases más. La Stateclase y la Cityclase tienen una relación cíclica hiop porque una Stateinstancia tiene una referencia a su capital Cityy a Citytiene una referencia a Stateen la que se encuentra.

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