Stack Overflow est une erreur de débordement de la pile d'appels (java.lang.StackOverflowError) qui se produit lorsque la profondeur maximale de la pile d'un thread est dépassée. Selon la Java Virtual Machine Specification, la profondeur typique de la pile dans la JVM est de 1024 cadres pour les systèmes 64 bits. La cause principale est une récursion infinie sans condition de base.
Points clés
StackOverflowError est une erreur fatale de la Machine Virtuelle Java (JVM) ou d'Android Runtime (ART) qui se produit lorsque la pile d'appels d'un thread atteint la profondeur maximale autorisée. Contrairement à OutOfMemoryError (épuisement du Heap), StackOverflowError est lié à une zone mémoire différente — la pile, où sont stockés les cadres d'appels de méthodes et les variables locales.
Chaque appel de méthode crée un cadre dans la pile : adresse de retour, paramètres et variables locales. Lors du retour de la méthode, le cadre est détruit. Si une méthode s'appelle elle-même (récursion) sans condition de base, les cadres s'accumulent jusqu'à remplir la pile. La JVM ne peut pas allouer un nouveau cadre et lance StackOverflowError avec un message « null » (en Java) ou avec l'indication d'une ligne de pile qui se répète à l'infini.
La taille de la pile d'un thread est fixée à la création et ne change pas pendant l'exécution. Sous Android, la taille typique de la pile du thread principal est de 32–48 Ko, ce qui donne une profondeur d'environ 512–1024 cadres pour des méthodes sans trop de variables locales. Pour les threads d'arrière-plan, la taille par défaut est plus petite — 16–24 Ko.
La pile d'appels (Call Stack) est une structure de données LIFO (Last In, First Out) qui gère l'ordre d'exécution des méthodes. Chaque fois que le programme appelle une méthode, la JVM crée un cadre dans la pile et le place au sommet. Lorsque la méthode se termine, le cadre est retiré.
Chaque cadre contient : pile d'opérandes (pour les instructions bytecode), tableau de variables locales (incluant this), référence au pool de constantes et adresse de retour. Plus une méthode a de variables locales, plus la taille de son cadre est grande et moins on peut appeler de méthodes avant de remplir la pile. Une méthode avec 10 paramètres et 20 variables locales occupe environ 3 fois plus d'espace qu'une méthode sans paramètres.
Sous Android, ART utilise sa propre implémentation de pile, différente de la JVM de bureau. ART peut augmenter dynamiquement la pile dans certaines limites, mais il existe toujours une limite stricte pour chaque thread. Le thread principal (thread UI) a la pile la plus grande, car il gère tout le cycle de vie de l'Activity et le traitement des événements.
// Récursion qui mène à StackOverflowError
fun recursiveCall(depth: Int): Int {
return recursiveCall(depth + 1) // pas de condition de base
}
// L'appel provoquera StackOverflowError à une profondeur de ~1000
recursiveCall(0)
Cinq scénarios typiques mènent à StackOverflowError dans les applications mobiles. La plupart sont liés à la récursion, mais il existe aussi des causes moins évidentes.
La cause la plus courante. Le développeur écrit une méthode récursive sans condition d'arrêt ou avec une condition qui ne devient jamais true. Chaque appel ajoute un cadre, et la pile se remplit en 500–2000 itérations selon la taille du cadre. Un exemple typique : calcul de la factorielle n! sans vérifier n == 0.
Vérifiez la condition de base au début de chaque méthode récursive. En Kotlin, utilisez require() ou check() pour valider les paramètres au début. Pour une récursion profonde (plus de 100 niveaux), envisagez de la remplacer par une approche itérative.
La classe A crée une instance de B, la classe B crée une instance de A — c'est une dépendance cyclique dans les constructeurs. En essayant de créer A, le constructeur de B est appelé, qui appelle le constructeur de A, et ainsi de suite jusqu'à StackOverflowError. Les frameworks DI (Dagger, Hilt) détectent ces cycles à la compilation, mais la création manuelle d'objets ne les détecte pas.
Utilisez l'Injection de Dépendances avec des graphes de dépendances : Dagger ou Koin vérifient les cycles au moment de la construction. Si le cycle est inévitable, remplacez la dépendance directe par une interface avec initialisation paresseuse ou une fabrique Provider.
// Dépendance cyclique — StackOverflowError
class A(private val b: B)
class B(private val a: A)
// Solution paresseuse
class A(private val bProvider: Provider<B>)
Le parcours d'un arbre View (ViewGroup.getChildAt()), d'un système de fichiers ou d'une structure JSON via la récursion peut dépasser la limite de la pile à une profondeur de plus de 500–1000 éléments. Un ViewGroup Android avec 20 niveaux d'imbrication est rare, mais l'analyse récursive de JSON avec 2000 objets imbriqués est un scénario réel.
Remplacez le parcours récursif par un parcours itératif utilisant un Stack<T> explicite ou ArrayDeque. Cela élimine complètement le risque de débordement de pile, car les objets du heap ne sont pas limités par la pile. BFS (Breadth-First Search) via Queue résout également le problème.
Une cause spécifique à Android : des appels cycliques de méthodes du cycle de vie lorsque la configuration est mal gérée. Par exemple, appeler recreate() dans onConfigurationChanged, qui appelle à nouveau onConfigurationChanged, et ainsi de suite jusqu'à StackOverflowError. De même : setContentView() dans onLayout(), qui déclenche une autre mesure et un autre layout.
N'appelez pas recreate() dans les méthodes liées aux changements de configuration. Pour mettre à jour l'UI lors d'un changement de thème, utilisez setTheme() sans recreate. Pour les changements dynamiques d'orientation — appelez requestOrientation() une fois, sans indicateur dans la configuration.
Gson, Moshi ou Kotlin Serialization en essayant de sérialiser un objet avec des références cycliques (A référence B, B référence A) entrent dans une récursion infinie et échouent avec StackOverflowError. C'est un problème courant lors de la sérialisation d'entités avec des relations bidirectionnelles (JPA, Room avec ForeignKey).
Utilisez @Transient, @JsonIgnore ou @kotlinx.serialization.Transient pour un côté du cycle. Pour Gson — JsonSerializer avec une limite de profondeur explicite. Pour Room — ne sérialisez jamais Entity directement, utilisez des mappeurs DTO.
Diagnostiquer StackOverflowError est plus facile que d'autres erreurs mémoire : la trace de pile montre dans la plupart des cas une séquence répétée d'appels. Cela indique immédiatement une récursion.
La trace de pile de StackOverflowError est unique : après les premières 200–500 lignes, le même motif d'appels commence à se répéter. La JVM tronque les lignes répétées à la fin et affiche « ... 1234 more ». Le nombre de lignes non répétées avant « ... » indique la profondeur de récursion qui a causé l'erreur.
Lisez les premières lignes de la trace de pile — elles montrent par quelle méthode la répétition a commencé. Trouvez la méthode qui s'appelle elle-même ou crée une chaîne d'appels qui revient à elle. Corrigez la condition de base ou remplacez la récursion par une boucle.
Temporairement, le problème peut être résolu en augmentant la taille de la pile via le flag JVM -Xss. Pour Android, la taille de la pile est définie via AndroidManifest : android:largeHeap n'affecte pas la pile. Pour augmenter la pile du thread dans le code : Thread(ThreadGroup, Runnable, name, stackSize). stackSize est la taille souhaitée en octets.
// Création d'un thread avec une pile augmentée
val thread = Thread(null, runnable, "big-stack-thread", 64 * 1024)
thread.start()
Important : augmenter la pile ne résout pas le problème, il le retarde seulement. Avec une récursion de 10 000 niveaux, une pile de 64 Ko sera remplacée par une pile de 128 Ko, donnant 20 000 niveaux — mais l'erreur se produira toujours, seulement plus tard. La seule solution correcte est le remplacement itératif de la récursion.
Les algorithmes itératifs n'utilisent pas la pile d'appels pour stocker les états intermédiaires — ils les stockent dans le heap (Stack<T> ou ArrayDeque). Parcours d'arbre binaire, calcul de factorielle, Fibonacci — toute récursion peut être convertie en itération à l'aide d'une pile explicite.
// Parcours itératif d'arbre — aucun risque 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) }
}
}
La prévention de StackOverflowError est un ensemble de règles et d'outils qui identifient les cycles récursifs potentiels avant qu'ils n'atteignent la production.
Ajoutez un compteur de profondeur protecteur dans les méthodes récursives dans les builds de débogage. Si la profondeur dépasse un seuil (par exemple, 1000), lancez une exception avec un message clair. Cela transforme un StackOverflowError avec une trace illisible en une exception métier compréhensible.
fun safeRecursive(n: Int, depth: Int = 0): Int {
if (depth > 1000) {
throw IllegalStateException("La récursion a dépassé 1000 niveaux")
}
return if (n <= 1) n
else safeRecursive(n - 1, depth + 1)
}
Detekt (Kotlin) et Infer (Facebook) trouvent les récursions infinies potentielles au niveau de l'analyse statique. Detekt a une règle PotentiallyInfiniteRecursion qui avertit des auto-appels sans changement de paramètres. Activez-la dans l'ensemble de règles CI et définissez la sévérité sur error.
Lors de la revue de code, faites attention à : toute méthode avec auto-appel, appels récursifs dans les lambdas (fonctions inline Kotlin), appels cycliques entre différentes classes, récursion dans les délégations de propriétés. Pour chaque méthode récursive, vérifiez : y a-t-il une condition de base, le paramètre change-t-il à chaque étape, le changement de paramètre garantit-il l'atteinte de la condition de base.
Kotlin prend en charge le modificateur tailrec : si une méthode récursive est marquée tailrec et que l'appel est terminal (dernière opération), le compilateur la convertit en itération. Cependant, tailrec fonctionne uniquement pour l'auto-appel (la méthode s'appelle elle-même directement), ne fonctionne pas pour la récursion mutuelle et n'est pas pris en charge dans les versions de Kotlin compatibles Android antérieures à 1.5.
tailrec fun factorial(n: Int, acc: Int = 1): Int {
return if (n <= 1) acc
else factorial(n - 1, acc * n) // appel terminal
}
Questions fréquentes
Oui, mais seulement au niveau Java. Error, comme Exception, est un Throwable. Cependant, après StackOverflowError, la pile est endommagée — les cadres qui n'ont pas pu tenir ne peuvent pas se terminer correctement. Tenter de créer un nouvel objet dans le bloc catch peut provoquer un autre StackOverflowError.
Pour le thread principal — 32–48 Ko, pour les threads d'arrière-plan — 16–24 Ko. La taille exacte dépend de la version d'Android et du fabricant de l'appareil. ART utilise une expansion dynamique de la pile, mais pas plus de 2× la valeur initiale.
En Kotlin — oui, si la méthode est marquée tailrec. Le compilateur convertit la récursion terminale en itération, éliminant complètement la croissance de la pile. En Java, la récursion terminale n'est pas optimisée par la JVM (contrairement aux langages fonctionnels comme Scala).
La taille de la pile sur l'émulateur et sur un appareil réel peut différer. L'émulateur utilise une JVM de bureau avec une pile typique de 512–1024 Ko, tandis qu'Android ART utilise 32–48 Ko. L'erreur se manifestera sur ART plus tôt que sur la JVM de bureau.
Zone mémoire : StackOverflowError est une erreur de pile (cadres d'appels), OutOfMemoryError est une erreur de heap (objets). StackOverflowError est presque toujours causé par une récursion, tandis que OutOfMemoryError est causé par des fuites mémoire ou de gros objets.
Résumé
Nous développerons une application mobile clé en main
IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.
Lisez aussi