Stack Overflow es un error de desbordamiento de la pila de llamadas (java.lang.StackOverflowError) que ocurre cuando se supera la profundidad máxima de la pila de un hilo. Según la Java Virtual Machine Specification, la profundidad típica de la pila en JVM es de 1024 marcos para sistemas de 64 bits. La causa principal es la recursión infinita sin una condición base.
Puntos clave
StackOverflowError es un error fatal de la Máquina Virtual de Java (JVM) o Android Runtime (ART) que ocurre cuando la pila de llamadas de un hilo alcanza la profundidad máxima permitida. A diferencia de OutOfMemoryError (agotamiento del Heap), StackOverflowError está relacionado con un área de memoria diferente: la pila, donde se almacenan los marcos de llamadas a métodos y las variables locales.
Cada llamada a un método crea un marco en la pila: dirección de retorno, parámetros y variables locales. Al regresar del método, el marco se destruye. Si un método se llama a sí mismo (recursión) sin una condición base, los marcos se acumulan hasta llenar la pila. La JVM no puede asignar un nuevo marco y lanza StackOverflowError con un mensaje «null» (en Java) o con la indicación de una línea de pila que se repite infinitamente.
El tamaño de la pila de un hilo se fija en la creación y no cambia durante la ejecución. En Android, el tamaño típico de la pila del hilo principal es de 32–48 KB, lo que da una profundidad de aproximadamente 512–1024 marcos para métodos sin muchas variables locales. Para los hilos en segundo plano, el tamaño predeterminado es menor: 16–24 KB.
La pila de llamadas (Call Stack) es una estructura de datos LIFO (Last In, First Out) que gestiona el orden de ejecución de los métodos. Cada vez que el programa llama a un método, la JVM crea un marco en la pila y lo coloca encima. Al finalizar el método, el marco se extrae.
Cada marco contiene: pila de operandos (para instrucciones de bytecode), array de variables locales (incluyendo this), referencia al pool de constantes y dirección de retorno. Cuantas más variables locales tenga un método, mayor será el tamaño de su marco y menos métodos se podrán llamar antes de llenar la pila. Un método con 10 parámetros y 20 variables locales ocupa aproximadamente 3 veces más espacio que un método sin parámetros.
En Android, ART utiliza su propia implementación de pila, diferente de la JVM de escritorio. ART puede aumentar la pila dinámicamente dentro de ciertos límites, pero aún existe un límite estricto para cada hilo. El hilo principal (hilo de UI) tiene la pila más grande, ya que maneja todo el ciclo de vida de Activity y el procesamiento de eventos.
// Recursión que lleva a StackOverflowError
fun recursiveCall(depth: Int): Int {
return recursiveCall(depth + 1) // sin condición base
}
// La llamada causará StackOverflowError a una profundidad de ~1000
recursiveCall(0)
Cinco escenarios típicos provocan StackOverflowError en aplicaciones móviles. La mayoría están relacionados con la recursión, pero también hay causas menos evidentes.
La causa más común. El desarrollador escribe un método recursivo sin condición de parada o con una condición que nunca se vuelve true. Cada llamada añade un marco y la pila se llena en 500–2000 iteraciones según el tamaño del marco. Un ejemplo típico: calcular el factorial n! sin verificar n == 0.
Verifique la condición base al inicio de cada método recursivo. En Kotlin, use require() o check() para validar parámetros al inicio. Para recursión profunda (más de 100 niveles), considere reemplazarla con un enfoque iterativo.
La clase A crea una instancia de B, la clase B crea una instancia de A — esto es una dependencia cíclica en constructores. Al intentar crear A, se llama al constructor de B, que llama al constructor de A, y así hasta StackOverflowError. Los frameworks de DI (Dagger, Hilt) detectan estos ciclos en tiempo de compilación, pero la creación manual de objetos no los capta.
Utilice Inyección de Dependencias con grafos de dependencias: Dagger o Koin verifican ciclos en tiempo de compilación. Si el ciclo es inevitable, reemplace la dependencia directa por una interfaz con inicialización lazy o una fábrica Provider.
// Dependencia cíclica — StackOverflowError
class A(private val b: B)
class B(private val a: A)
// Solución lazy
class A(private val bProvider: Provider<B>)
El recorrido de un árbol View (ViewGroup.getChildAt()), sistema de archivos o estructura JSON mediante recursión puede superar el límite de la pila a una profundidad de más de 500–1000 elementos. Un ViewGroup de Android con 20 niveles de anidamiento es raro, pero el análisis recursivo de JSON con 2000 objetos anidados es un escenario real.
Reemplace el recorrido recursivo por uno iterativo usando un Stack<T> explícito o ArrayDeque. Esto elimina por completo el riesgo de desbordamiento de pila, ya que los objetos en el heap no están limitados por la pila. BFS (Breadth-First Search) mediante Queue también soluciona el problema.
Una causa específica de Android: llamadas cíclicas de métodos del ciclo de vida cuando la configuración se maneja incorrectamente. Por ejemplo, llamar a recreate() dentro de onConfigurationChanged, que vuelve a llamar a onConfigurationChanged, y así hasta StackOverflowError. De manera similar: setContentView() dentro de onLayout(), que desencadena otra medición y diseño.
No llame a recreate() dentro de métodos relacionados con cambios de configuración. Para actualizar la UI al cambiar el tema, use setTheme() sin recreate. Para cambios dinámicos de orientación — llame a requestOrientation() una vez, sin una bandera en la configuración.
Gson, Moshi o Kotlin Serialization al intentar serializar un objeto con referencias cíclicas (A referencia a B, B referencia a A) entran en recursión infinita y fallan con StackOverflowError. Este es un problema común al serializar entidades con relaciones bidireccionales (JPA, Room con ForeignKey).
Use @Transient, @JsonIgnore o @kotlinx.serialization.Transient para un lado del ciclo. Para Gson — JsonSerializer con un límite de profundidad explícito. Para Room — nunca serialice Entity directamente, use mapeadores DTO.
Diagnosticar StackOverflowError es más fácil que otros errores de memoria: el stack trace en la mayoría de los casos muestra una secuencia repetida de llamadas. Esto apunta inmediatamente a la recursión.
El stack trace de StackOverflowError es único: después de las primeras 200–500 líneas, comienza a repetirse el mismo patrón de llamadas. La JVM trunca las líneas repetidas al final y muestra «... 1234 more». La cantidad de líneas no repetidas antes de «...» indica la profundidad de recursión que causó el error.
Lea las primeras líneas del stack trace: muestran con qué método comenzó la repetición. Encuentre el método que se llama a sí mismo o crea una cadena de llamadas que vuelve a él. Corrija la condición base o reemplace la recursión con un bucle.
Temporalmente, el problema se puede resolver aumentando el tamaño de la pila mediante el flag de JVM -Xss. Para Android, el tamaño de la pila se establece mediante AndroidManifest: android:largeHeap no afecta la pila. Para aumentar la pila del hilo en código: Thread(ThreadGroup, Runnable, name, stackSize). stackSize es el tamaño deseado en bytes.
// Creación de un hilo con pila aumentada
val thread = Thread(null, runnable, "big-stack-thread", 64 * 1024)
thread.start()
Importante: aumentar la pila no resuelve el problema, solo lo retrasa. Con recursión de 10 000 niveles, una pila de 64 KB se reemplazará por una de 128 KB, dando 20 000 niveles — pero el error seguirá ocurriendo, solo que más tarde. La única solución correcta es el reemplazo iterativo de la recursión.
Los algoritmos iterativos no usan la pila de llamadas para almacenar estados intermedios — los almacenan en el heap (Stack<T> o ArrayDeque). Recorrido de árbol binario, cálculo de factorial, Fibonacci — cualquier recursión se puede convertir en iteración usando una pila explícita.
// Recorrido iterativo de árbol — sin riesgo de StackOverflow
fun traverseIterative(root: Node?) {
val stack = ArrayDeque<Node>()
stack.push(root)
while (stack.isNotEmpty()) {
val node = stack.pop() ?: continue
process(node)
node.right?.let { stack.push(it) }
node.left?.let { stack.push(it) }
}
}
Prevenir StackOverflowError es un conjunto de reglas y herramientas que identifican posibles ciclos recursivos antes de que lleguen a producción.
Añada un contador de profundidad protector en métodos recursivos en builds de depuración. Si la profundidad supera un umbral (por ejemplo, 1000), lance una excepción con un mensaje claro. Esto convierte un StackOverflowError con un trace ilegible en una excepción de negocio comprensible.
fun safeRecursive(n: Int, depth: Int = 0): Int {
if (depth > 1000) {
throw IllegalStateException("La recursión superó los 1000 niveles")
}
return if (n <= 1) n
else safeRecursive(n - 1, depth + 1)
}
Detekt (Kotlin) e Infer (Facebook) encuentran posibles recursiones infinitas a nivel de análisis estático. Detekt tiene una regla PotentiallyInfiniteRecursion que advierte sobre auto-llamadas sin cambio de parámetros. Actívela en el conjunto de reglas de CI y configure la severidad como error.
En la revisión de código, preste atención a: cualquier método con auto-llamada, llamadas recursivas dentro de lambdas (funciones inline de Kotlin), llamadas cíclicas entre diferentes clases, recursión en property delegates. Para cada método recursivo, verifique: si hay una condición base, si el parámetro cambia en cada paso, si el cambio de parámetro garantiza alcanzar la condición base.
Kotlin admite el modificador tailrec: si un método recursivo está marcado como tailrec y la llamada es tail (última operación), el compilador lo convierte en iteración. Sin embargo, tailrec solo funciona para auto-llamadas (el método se llama a sí mismo directamente), no funciona para recursión mutua y no es compatible en versiones de Kotlin compatibles con Android anteriores a la 1.5.
tailrec fun factorial(n: Int, acc: Int = 1): Int {
return if (n <= 1) acc
else factorial(n - 1, acc * n) // llamada tail
}
Preguntas frecuentes
Sí, pero solo a nivel de Java. Error, al igual que Exception, es un Throwable. Sin embargo, después de StackOverflowError la pila está dañada — los marcos que no cabían no pueden completarse correctamente. Intentar crear un nuevo objeto en el bloque catch puede provocar otro StackOverflowError.
Para el hilo principal — 32–48 KB, para hilos en segundo plano — 16–24 KB. El tamaño exacto depende de la versión de Android y del fabricante del dispositivo. ART usa expansión dinámica de pila, pero no más de 2× el valor inicial.
En Kotlin — sí, si el método está marcado como tailrec. El compilador convierte la recursión tail en iteración, eliminando por completo el crecimiento de la pila. En Java, la recursión tail no se optimiza en JVM (a diferencia de lenguajes funcionales como Scala).
El tamaño de la pila en el emulador y en un dispositivo real puede diferir. El emulador usa JVM de escritorio con una pila típica de 512–1024 KB, mientras que Android ART usa 32–48 KB. El error se manifestará en ART antes que en JVM de escritorio.
Área de memoria: StackOverflowError es un error de pila (marcos de llamadas), OutOfMemoryError es un error de heap (objetos). StackOverflowError casi siempre es causado por recursión, mientras que OutOfMemoryError es causado por fugas de memoria u objetos grandes.
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.