1. Información general
StackOverflowError puede ser molesto para los desarrolladores de Java, ya que es uno de los errores de tiempo de ejecución más comunes que podemos encontrar.
En este artículo, veremos cómo puede ocurrir este error al observar una variedad de ejemplos de código y cómo podemos solucionarlo.
2. Stack Frames y cómo se produce StackOverflowError
Empecemos con lo básico. Cuando se llama a un método, se crea un nuevo marco de pila en la pila de llamadas. Este marco de pila contiene parámetros del método invocado, sus variables locales y la dirección de retorno del método, es decir, el punto desde el cual la ejecución del método debe continuar después de que el método invocado haya regresado.
La creación de marcos de pila continuará hasta que llegue al final de las invocaciones de métodos que se encuentran dentro de los métodos anidados.
Durante este proceso, si JVM encuentra una situación en la que no hay espacio para crear un nuevo marco de pila, generará un StackOverflowError .
La causa más común para que la JVM se encuentre con esta situación es la recursión infinita / sin terminar : la descripción de Javadoc para StackOverflowError menciona que el error se produce como resultado de una recursividad demasiado profunda en un fragmento de código en particular.
Sin embargo, la recursividad no es la única causa de este error. También puede suceder en una situación en la que una aplicación sigue llamando a métodos desde dentro de los métodos hasta que se agota la pila . Este es un caso raro ya que ningún desarrollador seguiría intencionalmente malas prácticas de codificación. Otra causa poco común es tener una gran cantidad de variables locales dentro de un método .
El StackOverflowError también puede ser lanzado cuando una aplicación está diseñada para tener c relaciones entre las clases yclic . En esta situación, se llama repetidamente a los constructores de cada uno, lo que provoca que se produzca este error. Esto también se puede considerar como una forma de recursividad.
Otro escenario interesante que causa este error es si se crea una instancia de una clase dentro de la misma clase que una variable de instancia de esa clase . Esto hará que el constructor de la misma clase sea llamado una y otra vez (recursivamente) lo que eventualmente resultará en un StackOverflowError.
En la siguiente sección, veremos algunos ejemplos de código que demuestran estos escenarios.
3. StackOverflowError en acción
En el ejemplo que se muestra a continuación, se lanzará un StackOverflowError debido a una recursividad no intencionada, donde el desarrollador ha olvidado especificar una condición de terminación para el comportamiento recursivo:
public class UnintendedInfiniteRecursion { public int calculateFactorial(int number) { return number * calculateFactorial(number - 1); } }
Aquí, el error se produce en todas las ocasiones para cualquier valor que se pase al método:
public class UnintendedInfiniteRecursionManualTest { @Test(expected = StackOverflowError.class) public void givenPositiveIntNoOne_whenCalFact_thenThrowsException() { int numToCalcFactorial= 1; UnintendedInfiniteRecursion uir = new UnintendedInfiniteRecursion(); uir.calculateFactorial(numToCalcFactorial); } @Test(expected = StackOverflowError.class) public void givenPositiveIntGtOne_whenCalcFact_thenThrowsException() { int numToCalcFactorial= 2; UnintendedInfiniteRecursion uir = new UnintendedInfiniteRecursion(); uir.calculateFactorial(numToCalcFactorial); } @Test(expected = StackOverflowError.class) public void givenNegativeInt_whenCalcFact_thenThrowsException() { int numToCalcFactorial= -1; UnintendedInfiniteRecursion uir = new UnintendedInfiniteRecursion(); uir.calculateFactorial(numToCalcFactorial); } }
Sin embargo, en el siguiente ejemplo se especifica una condición de terminación, pero nunca se cumple si se pasa un valor de -1 al método calculateFactorial () , lo que provoca una recursión infinita / no terminada:
public class InfiniteRecursionWithTerminationCondition { public int calculateFactorial(int number) { return number == 1 ? 1 : number * calculateFactorial(number - 1); } }
Este conjunto de pruebas demuestra este escenario:
public class InfiniteRecursionWithTerminationConditionManualTest { @Test public void givenPositiveIntNoOne_whenCalcFact_thenCorrectlyCalc() { int numToCalcFactorial = 1; InfiniteRecursionWithTerminationCondition irtc = new InfiniteRecursionWithTerminationCondition(); assertEquals(1, irtc.calculateFactorial(numToCalcFactorial)); } @Test public void givenPositiveIntGtOne_whenCalcFact_thenCorrectlyCalc() { int numToCalcFactorial = 5; InfiniteRecursionWithTerminationCondition irtc = new InfiniteRecursionWithTerminationCondition(); assertEquals(120, irtc.calculateFactorial(numToCalcFactorial)); } @Test(expected = StackOverflowError.class) public void givenNegativeInt_whenCalcFact_thenThrowsException() { int numToCalcFactorial = -1; InfiniteRecursionWithTerminationCondition irtc = new InfiniteRecursionWithTerminationCondition(); irtc.calculateFactorial(numToCalcFactorial); } }
En este caso particular, el error podría haberse evitado por completo si la condición de terminación se hubiera puesto simplemente como:
public class RecursionWithCorrectTerminationCondition { public int calculateFactorial(int number) { return number <= 1 ? 1 : number * calculateFactorial(number - 1); } }
Aquí está la prueba que muestra este escenario en la práctica:
public class RecursionWithCorrectTerminationConditionManualTest { @Test public void givenNegativeInt_whenCalcFact_thenCorrectlyCalc() { int numToCalcFactorial = -1; RecursionWithCorrectTerminationCondition rctc = new RecursionWithCorrectTerminationCondition(); assertEquals(1, rctc.calculateFactorial(numToCalcFactorial)); } }
Ahora veamos un escenario donde el StackOverflowError ocurre como resultado de relaciones cíclicas entre clases. Consideremos ClassOne y ClassTwo , que se instancian entre sí dentro de sus constructores causando una relación cíclica:
public class ClassOne { private int oneValue; private ClassTwo clsTwoInstance = null; public ClassOne() { oneValue = 0; clsTwoInstance = new ClassTwo(); } public ClassOne(int oneValue, ClassTwo clsTwoInstance) { this.oneValue = oneValue; this.clsTwoInstance = clsTwoInstance; } }
public class ClassTwo { private int twoValue; private ClassOne clsOneInstance = null; public ClassTwo() { twoValue = 10; clsOneInstance = new ClassOne(); } public ClassTwo(int twoValue, ClassOne clsOneInstance) { this.twoValue = twoValue; this.clsOneInstance = clsOneInstance; } }
Ahora digamos que intentamos crear una instancia de ClassOne como se ve en esta prueba:
public class CyclicDependancyManualTest { @Test(expected = StackOverflowError.class) public void whenInstanciatingClassOne_thenThrowsException() { ClassOne obj = new ClassOne(); } }
Esto termina con un StackOverflowError ya que el constructor de ClassOne está instanciando ClassTwo, y el constructor de ClassTwo nuevamente está instanciando ClassOne. Y esto sucede repetidamente hasta que desborda la pila.
A continuación, veremos qué sucede cuando se crea una instancia de una clase dentro de la misma clase que una variable de instancia de esa clase.
Como se ve en el siguiente ejemplo, AccountHolder se instancia a sí mismo como una variable de instancia jointAccountHolder :
public class AccountHolder { private String firstName; private String lastName; AccountHolder jointAccountHolder = new AccountHolder(); }
Cuando se crea una instancia de la clase AccountHolder , se lanza un StackOverflowError debido a la llamada recursiva del constructor como se ve en esta prueba:
public class AccountHolderManualTest { @Test(expected = StackOverflowError.class) public void whenInstanciatingAccountHolder_thenThrowsException() { AccountHolder holder = new AccountHolder(); } }
4. Manejo de StackOverflowError
Lo mejor que se puede hacer cuando se encuentra un StackOverflowError es inspeccionar el seguimiento de la pila con cuidado para identificar el patrón repetido de números de línea. Esto nos permitirá localizar el código que tiene una recursividad problemática.
Examinemos algunos rastros de pila causados por los ejemplos de código que vimos anteriormente.
Este seguimiento de pila es producido por InfiniteRecursionWithTerminationConditionManualTest si omitimos la declaración de excepción esperada :
java.lang.StackOverflowError at c.b.s.InfiniteRecursionWithTerminationCondition .calculateFactorial(InfiniteRecursionWithTerminationCondition.java:5) at c.b.s.InfiniteRecursionWithTerminationCondition .calculateFactorial(InfiniteRecursionWithTerminationCondition.java:5) at c.b.s.InfiniteRecursionWithTerminationCondition .calculateFactorial(InfiniteRecursionWithTerminationCondition.java:5) at c.b.s.InfiniteRecursionWithTerminationCondition .calculateFactorial(InfiniteRecursionWithTerminationCondition.java:5)
Aquí, se puede ver la línea número 5 repitiéndose. Aquí es donde se realiza la llamada recursiva. Ahora solo es cuestión de examinar el código para ver si la recursividad se realiza de manera correcta.
Aquí está el seguimiento de la pila que obtenemos al ejecutar CyclicDependancyManualTest (nuevamente, sin la excepción esperada ):
java.lang.StackOverflowError at c.b.s.ClassTwo.(ClassTwo.java:9) at c.b.s.ClassOne.(ClassOne.java:9) at c.b.s.ClassTwo.(ClassTwo.java:9) at c.b.s.ClassOne.(ClassOne.java:9)
Este seguimiento de pila muestra los números de línea que causan el problema en las dos clases que están en una relación cíclica. La línea número 9 de ClassTwo y la línea número 9 de ClassOne apuntan a la ubicación dentro del constructor donde intenta instanciar la otra clase.
Una vez que el código se haya inspeccionado a fondo y si ninguno de los siguientes (o cualquier otro error de lógica de código) es la causa del error:
- Recursividad implementada incorrectamente (es decir, sin condición de terminación)
- Dependencia cíclica entre clases
- Instanciar una clase dentro de la misma clase como una variable de instancia de esa clase
Sería una buena idea intentar aumentar el tamaño de la pila. Según la JVM instalada, el tamaño de pila predeterminado puede variar.
La bandera -Xss se puede usar para aumentar el tamaño de la pila, ya sea desde la configuración del proyecto o desde la línea de comandos.
5. Conclusión
En este artículo, analizamos más de cerca el StackOverflowError, incluido cómo el código Java puede causarlo y cómo podemos diagnosticarlo y solucionarlo.
El código fuente relacionado con este artículo se puede encontrar en GitHub.