Stack Overflow in mobiele ontwikkeling — wat is het, oorzaken en preventiemethoden

Auteur: IT Sectr Gepubliceerd: 2026-03-29 Leestijd: 9 min

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 — JVM-fout bij overschrijding van de dieptelimiet van de call-stack
  • Stackdiepte is beperkt en bedraagt 512–2048 frames afhankelijk van de configuratie
  • Oneindige recursie — de meest voorkomende oorzaak van StackOverflowError
  • Staartrecursie wordt niet geoptimaliseerd in JVM, in tegenstelling tot functionele talen
  • Iteratieve vervanging van recursie — een betrouwbare manier om overloop te voorkomen

Wat is Stack Overflow

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.

Hoe de call-stack werkt

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.

kotlin
// 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)

Belangrijkste oorzaken van stackoverloop

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.

Oneindige recursie zonder basisconditie

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.

Cyclische afhankelijkheden in constructors

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.

kotlin
// Cyclische afhankelijkheid — StackOverflowError
class A(private val b: B)
class B(private val a: A)

// Lazy-oplossing
class A(private val bProvider: Provider<B>)

Diepe recursie bij het doorlopen van grafen

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.

Onjuiste verwerking van onConfigurationChanged

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.

Serialisatie met cyclische verwijzingen

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.

Hoe StackOverflowError te diagnosticeren en te verhelpen

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.

Lezen van de stack-trace

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.

Vergroten van de stackgrootte (tijdelijke oplossing)

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.

kotlin
// 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.

Recursie vervangen door iteratie

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.

kotlin
// 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) }
    }
}

Hoe Stack Overflow te voorkomen

Preventie van StackOverflowError is een set regels en tools die potentiële recursieve cycli detecteren voordat ze in productie komen.

Dieptelimiet voor recursie in debug-build

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.

kotlin
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)
}

Statische code-analyse

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.

Code Review gericht op recursie

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.

Staart-recursieve transformatie (beperkt)

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.

kotlin
tailrec fun factorial(n: Int, acc: Int = 1): Int {
    return if (n <= 1) acc
           else factorial(n - 1, acc * n) // staartaanroep
}

Veelgestelde vragen

Kan StackOverflowError worden opgevangen met try-catch?

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.

Wat is de standaard stackgrootte in Android?

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.

Kan staartrecursie StackOverflowError voorkomen?

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).

Waarom treedt StackOverflowError op in de emulator maar niet op het apparaat?

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.

Wat is het verschil tussen StackOverflowError en OutOfMemoryError?

Geheugengebied: StackOverflowError — stackfout (aanroepframes), OutOfMemoryError — heapfout (objecten). StackOverflowError wordt bijna altijd veroorzaakt door recursie, terwijl OutOfMemoryError wordt veroorzaakt door geheugenlekken of grote objecten.

Samenvatting

  • StackOverflowError — overloop van de call-stack bij overschrijding van de dieptelimiet van recursie
  • Stackdiepte in Android is 512–1024 frames op de hoofdthread
  • Oneindige recursie — de belangrijkste oorzaak; controleer de basisconditie in elke recursieve methode
  • Cyclische afhankelijkheden in constructors — een minder voor de hand liggende maar veelvoorkomende oorzaak van overloop
  • Iteratieve vervanging van recursie via een expliciete Stack<T> elimineert het risico volledig
  • tailrec in Kotlin transformeert staartrecursie naar iteratie op compilerniveau
  • Statische analyse (Detekt, Infer) vindt potentieel oneindige recursies vóór uitvoering

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.

Bespreek het project

Lees ook