Stack Overflow — fout door overloop van de call-stack (java.lang.StackOverflowError), die optreedt bij overschrijding van de maximale diepte van de thread-stack. Volgens Java Virtual Machine Specification is de typische stackdiepte in JVM 1024 frames voor 64-bit systemen. De belangrijkste oorzaak is oneindige recursie zonder basisstopconditie.
Belangrijkste punten
StackOverflowError is een fatale fout van de Java Virtual Machine (JVM) of Android Runtime (ART), die optreedt wanneer de call-stack van een thread de maximaal toegestane diepte bereikt. In tegenstelling tot OutOfMemoryError (gebrek aan Heap), heeft StackOverflowError betrekking op een ander geheugengebied — de stack, waar frames van methodeaanroepen en lokale variabelen worden opgeslagen.
Elke methodeaanroep creëert een frame in de stack: retour adres, parameters en lokale variabelen. Bij terugkeer uit de methode wordt het frame vernietigd. Als een methode zichzelf aanroept (recursie) zonder basisconditie, stapelen de frames zich op tot de stack vol is. JVM kan geen nieuw frame toewijzen en gooit StackOverflowError met de melding „null” (in Java) of met een aanduiding van een oneindig herhalende stack-trace.
De grootte van de thread-stack wordt bij creatie vastgelegd en verandert niet tijdens uitvoering. In Android is de typische stackgrootte van de hoofdthread 32–48 KB, wat een diepte geeft van ongeveer 512–1024 frames voor methoden zonder veel lokale variabelen. Voor achtergrondthreads is de standaardgrootte kleiner — 16–24 KB.
De call-stack is een LIFO-gegevensstructuur (Last In, First Out) die de uitvoeringsvolgorde van methoden beheert. Elke keer dat een programma een methode aanroept, maakt JVM een frame in de stack en plaatst het bovenop. Bij voltooiing van de methode wordt het frame verwijderd.
Elk frame bevat: operand stack (operandstack voor bytecode-instructies), array of local variables (inclusief this), reference to constant pool en retour adres. Hoe meer lokale variabelen een methode heeft, hoe groter de framegrootte en hoe minder methoden kunnen worden aangeroepen voordat de stack vol is. Een methode met 10 parameters en 20 lokale variabelen neemt ongeveer 3 keer zoveel ruimte in als een methode zonder parameters.
Op Android gebruikt ART een eigen stackimplementatie, anders dan Desktop JVM. ART kan de stack in bepaalde mate dynamisch vergroten, maar voor elke thread geldt nog steeds een harde limiet. De hoofdthread (UI-thread) heeft de grootste stack omdat de volledige levenscyclus van Activity en gebeurtenisverwerking erop worden uitgevoerd.
// Recursie die leidt tot StackOverflowError
fun recursiveCall(depth: Int): Int {
return recursiveCall(depth + 1) // geen basisconditie
}
// Aanroep leidt tot StackOverflowError op diepte ~1000
recursiveCall(0)
Vijf typische scenario's leiden tot StackOverflowError in mobiele apps. De meeste zijn gerelateerd aan recursie, maar er zijn ook minder voor de hand liggende oorzaken.
De meest voorkomende oorzaak. De ontwikkelaar schrijft een recursieve methode zonder stopconditie of met een conditie die nooit true wordt. Elke aanroep voegt een frame toe en de stack raakt vol na 500–2000 iteraties, afhankelijk van de framegrootte. Typisch voorbeeld: berekening van faculteit n! zonder controle op n == 0.
Controleer de basisconditie aan het begin van elke recursieve methode. Gebruik in Kotlin require() of check() voor parametervalidatie aan het begin. Overweeg voor diepe recursie (meer dan 100 niveaus) vervanging door een iteratieve benadering.
Klasse A maakt een instantie van B, klasse B maakt een instantie van A — dit is een cyclische afhankelijkheid in constructors. Bij het proberen te maken van A wordt de constructor van B aangeroepen, die de constructor van A aanroept, en zo verder tot StackOverflowError. DI-frameworks (Dagger, Hilt) detecteren dergelijke cycli in de compilatiefase, maar handmatige objectcreatie vangt ze niet.
Gebruik Dependency Injection met afhankelijkheidsgrafen: Dagger of Koin controleren cycli in de buildfase. Als een cyclus onvermijdelijk is, vervang dan de directe afhankelijkheid door een interface met lazy-initialisatie of een Provider-fabriek.
// Cyclische afhankelijkheid — StackOverflowError
class A(private val b: B)
class B(private val a: A)
// Lazy-oplossing
class A(private val bProvider: Provider<B>)
Het doorlopen van een View-boom (ViewGroup.getChildAt()), bestandssysteem of JSON-structuur via recursie kan de stacklimiet overschrijden bij een diepte van meer dan 500–1000 elementen. Android ViewGroup met 20 niveaus nesting komt zelden voor, maar het recursief parsen van JSON met 2000 geneste objecten is een realistisch scenario.
Vervang recursief doorlopen door iteratief via een expliciete Stack<T> of ArrayDeque. Dit elimineert het risico op stackoverloop volledig, omdat objecten in de heap niet worden beperkt door de stacklimiet. BFS (Breadth-First Search) via Queue lost het probleem ook op.
Android-specifieke oorzaak: cyclische aanroep van levenscycli-methoden bij onjuiste configuratieverwerking. Bijvoorbeeld, in onConfigurationChanged wordt recreate() aangeroepen, wat opnieuw onConfigurationChanged aanroept, en zo verder tot StackOverflowError. Vergelijkbaar: setContentView() binnen onLayout(), wat herhaalde meting en layout veroorzaakt.
Roep recreate() niet aan binnen methoden die verband houden met configuratiewijzigingen. Gebruik voor UI-update bij themawijziging setTheme() zonder recreate. Voor dynamische oriëntatiewijziging — eenmalig requestOrientation(), zonder vlag in de configuratie.
Gson, Moshi of Kotlin Serialization gaan bij het serialiseren van een object met cyclische verwijzingen (A verwijst naar B, B verwijst naar A) in oneindige recursie en crashen met StackOverflowError. Dit is een veelvoorkomend probleem bij het serialiseren van Entity met bidirectional Relationship (JPA, Room met ForeignKey).
Gebruik @Transient, @JsonIgnore of @kotlinx.serialization.Transient voor een van de zijden van de cyclus. Voor Gson — JsonSerializer met expliciete dieptebeperking. Voor Room — serialiseer Entity nooit direct, gebruik DTO-mappers.
Diagnose van StackOverflowError is eenvoudiger dan andere geheugenfouten: de stack-trace toont in de meeste gevallen een herhalende reeks aanroepen. Dit wijst onmiddellijk op recursie.
De stack-trace van StackOverflowError is uniek: na de eerste 200–500 regels begint herhaling van hetzelfde aanroeppatroon. JVM kapt herhalende regels aan het einde af en toont „... 1234 more”. Het aantal niet-herhalende regels vóór „...” toont de diepte van de recursie die tot de fout heeft geleid.
Lees de eerste regels van de stack-trace — ze tonen bij welke methode de herhaling begon. Vind de methode die zichzelf aanroept of een aanroepketen creëert die naar haar terugkeert. Corrigeer de basisconditie of vervang recursie door een lus.
Tijdelijk kan het probleem worden opgelost door de stackgrootte te vergroten via de JVM-vlag -Xss. In Android wordt de stackgrootte ingesteld via AndroidManifest: android:largeHeap heeft geen invloed op de stack. Voor het vergroten van de thread-stack in code: Thread(ThreadGroup, Runnable, name, stackSize). stackSize — de gewenste grootte in bytes.
// Thread maken met vergrote stack
val thread = Thread(null, runnable, "big-stack-thread", 64 * 1024)
thread.start()
Belangrijk: het vergroten van de stack lost het probleem niet op, maar stelt het alleen uit. Bij recursie van 10.000 niveaus wordt een stack van 64 KB vervangen door een stack van 128 KB, wat 20.000 niveaus oplevert — maar de fout zal nog steeds optreden, alleen later. De enige juiste oplossing is iteratieve vervanging van recursie.
Iteratieve algoritmen gebruiken de call-stack niet voor het opslaan van tussenliggende toestanden — ze slaan ze op in de heap (Stack<T> of ArrayDeque). Het doorlopen van een binaire boom, faculteitsberekening, Fibonacci — elke recursie kan worden omgezet in iteratie via een expliciete stack.
// Iteratieve boomdoorloping — zonder risico op 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) }
}
}
Preventie van StackOverflowError is een set regels en tools die potentiële recursieve cycli detecteren voordat ze in productie komen.
Voeg een beschermende diepteteller toe aan recursieve methoden in de debug-build. Als de diepte een drempel overschrijdt (bijv. 1000), gooi dan een uitzondering met een duidelijke melding. Dit verandert StackOverflowError met onleesbare trace in een begrijpelijke bedrijfsuitzondering.
fun safeRecursive(n: Int, depth: Int = 0): Int {
if (depth > 1000) {
throw IllegalStateException("Recursie overschreed 1000 niveaus")
}
return if (n <= 1) n
else safeRecursive(n - 1, depth + 1)
}
Detekt (Kotlin) en Infer (Facebook) vinden potentieel oneindige recursies op het niveau van statische analyse. Detekt heeft de regel PotentiallyInfiniteRecursion die waarschuwt voor self-call zonder parameterwijziging. Voeg het toe aan de CI-regelset en stel severity in op error.
Let bij code review op: alle self-call methoden, recursieve aanroepen binnen lambda's (Kotlin inline-functies), cyclische aanroepen tussen verschillende klassen, recursie in property delegates. Controleer voor elke recursieve methode: is er een basisconditie, verandert de parameter bij elke stap, garandeert de parameterwijziging het bereiken van de basisconditie.
Kotlin ondersteunt de tailrec-modifier: als een recursieve methode is gemarkeerd met tailrec en de aanroep is een staartaanroep (laatste bewerking), transformeert de compiler deze naar iteratie. Echter, tailrec werkt alleen voor self-call (methode roept zichzelf direct aan), niet voor wederzijdse recursie, en wordt niet ondersteund in Android-compatibele Kotlin-versies vóór 1.5.
tailrec fun factorial(n: Int, acc: Int = 1): Int {
return if (n <= 1) acc
else factorial(n - 1, acc * n) // staartaanroep
}
Veelgestelde vragen
Ja, maar alleen op Java-niveau. Error is, net als Exception, een Throwable. Echter, na StackOverflowError is de stack beschadigd — frames die niet pasten kunnen niet correct worden afgerond. Het maken van een nieuw object in het catch-blok kan een nieuwe StackOverflowError veroorzaken.
Voor de hoofdthread — 32–48 KB, voor de achtergrondthread — 16–24 KB. De exacte grootte hangt af van de Android-versie en de fabrikant van het apparaat. ART gebruikt dynamische stackuitbreiding, maar niet meer dan 2× de beginwaarde.
In Kotlin — ja, als de methode is gemarkeerd met tailrec. De compiler transformeert staartrecursie naar iteratie, waardoor stackgroei volledig wordt geëlimineerd. In Java wordt staartrecursie niet geoptimaliseerd door JVM (in tegenstelling tot functionele talen zoals Scala).
De stackgrootte op de emulator en het echte apparaat kan verschillen. De emulator gebruikt Desktop JVM met een typische stack van 512–1024 KB, terwijl Android ART 32–48 KB gebruikt. De fout zal eerder optreden in ART dan in Desktop JVM.
Geheugengebied: StackOverflowError — stackfout (aanroepframes), OutOfMemoryError — heapfout (objecten). StackOverflowError wordt bijna altijd veroorzaakt door recursie, terwijl OutOfMemoryError wordt veroorzaakt door geheugenlekken of grote objecten.
Samenvatting
We ontwikkelen een mobiele applicatie turnkey
IT Sectr creëert sinds 2017 iOS- en Android-applicaties voor startups en bedrijven. We adviseren u en stellen de beste oplossing voor.
Lees ook