Geheugenlek: wat het is, typische scenario's en diagnostiek

Auteur: IT Sectr Gepubliceerd: 2026-07-29 Leestijd: 10 min

Geheugenlek (memory leak) — een situatie waarin de applicatie het geheugen niet vrijgeeft dat wordt ingenomen door objecten die niet langer nodig zijn. In mobiele ontwikkeling is dit bijzonder kritiek: beperkte heap en afwezigheid van swap leiden tot OutOfMemoryError en het crashen van de applicatie. Volgens Purdue University (2022) bevat 35% van de Android-applicaties in Google Play ten minste één geheugenlek. We bespreken typische scenario's, diagnostische hulpmiddelen en oplossingsmethoden.

Belangrijkste punten

  • GC Root — het toegangspunt waarmee de garbage collector levende objecten bepaalt
  • Context lek — het doorgeven van Activity Context aan een singleton leidt tot het vasthouden van de gehele View-hiërarchie
  • Handler met postDelayed — als Activity is vernietigd, laat Handler het niet naar GC gaan
  • Heap dump — de primaire methode voor het analyseren van lekken via MAT of Android Profiler
  • SoftReference — alternatief voor WeakReference voor caches met automatische opschoning bij geheugentekort

Wat is een geheugenlek in mobiele applicaties?

Geheugenlek — is een situatie waarin toegewezen geheugen niet aan het systeem wordt teruggegeven nadat het object niet langer nodig is voor het programma. De garbage collector beschouwt een dergelijk object als levend omdat er een actieve referentieketen van GC Root naar leidt.

In Java/Kotlin werkt de garbage collector automatisch, maar hij kan niet bepalen dat een object logisch niet nodig is als er een technische referentie naar bestaat. De ontwikkelaar moet onnodige verbindingen expliciet verbreken. In Swift/Objective-C telt ARC automatisch referenties, maar retain cycles blokkeren het nulstellen van de teller.

Het grootste gevaar van lekken is het cumulatieve effect. Elk lek verbruikt een kleine hoeveelheid geheugen, maar bij meerdere overgangen tussen schermen (schermrotaties, openen/sluiten van Activity) hopen lekken zich op totdat de heap-limiet is bereikt.

Wat is het verschil tussen een lek en opzwellen?

Lek — het object is ontoegankelijk voor code, maar niet verwijderd door GC. Opzwellen — het object is logisch nodig, maar wordt in overmatige hoeveelheid opgeslagen. Voorbeeld van opzwellen: een afbeeldingcache van 100 MB met een werkset van 30 MB. Beide problemen leiden tot OOM, maar de oorzaken en behandelmethoden zijn verschillend.

Hoe werkt de garbage collector en waarom ontstaan lekken?

ART (Android Runtime) gebruikt generationele garbage collection met concurrent compaction. Het geheugen is verdeeld in jonge generatie (Young), oude (Old) en grote objecten (Large). Objecten die meerdere GC-cycli hebben overleefd, worden verplaatst naar Old generation, waar minder vaak wordt verzameld — dit versnelt normale cycli.

GC start wanneer de heap een bepaalde bezettingsdrempel bereikt (meestal 75-85%). Tijdens GC worden alle threads van de applicatie onderbroken (STW — Stop The World). Hoe meer levende objecten, hoe langer de pauze. Lekken verhogen het aantal levende objecten en verlengen GC-pauzes.

De collector bepaalt levende objecten door de graaf te doorlopen vanaf GC Roots: statische velden, stapelvariabelen van actieve threads, JNI-referenties. Elk object dat bereikbaar is via referenties van deze wortels, wordt als levend beschouwd — zelfs als de ontwikkelaar weet dat het niet langer nodig is.

kotlin
// Voorbeeld: statische collectie als GC Root — permanent lek
object GlobalHolder {
    val listeners = mutableListOf<WeakReference<Any>>()
}

class LeakingFragment : Fragment() {
    override fun onCreate(savedInstanceState: Bundle ?= null) {
        super.onCreate(savedInstanceState)
        GlobalHolder.listeners.add(WeakReference(this))
        // WeakReference blokkeert GC niet — correct gedrag
    }
}

WeakReference lost het probleem op: GC negeert weak-referenties bij het bepalen van levende objecten. Als er alleen weak-referenties naar een object overblijven, wordt het in de volgende GC-cyclus verzameld.

Typische lekscenario's in Android en iOS

Activity Context — het meest voorkomende lekscenario in Android. Als een singleton, statisch veld of langlevende service een referentie naar Activity Context bewaart, kan de gehele Activity met alle Views niet door GC worden verzameld. Oplossing: gebruik Application Context voor langlevende objecten.

Handler en verzonden berichten — Handler.postDelayed(runnable, delay) plaatst een bericht in de wachtrij van Main Looper. Als Activity is vernietigd voordat de vertraging is verstreken, staat het bericht nog in de wachtrij en houdt de referentie vast via Runnable → anonieme klasse → buitenste klasse (Activity).

kotlin
class SafeActivity : AppCompatActivity() {
    private val mainHandler = Handler(Looper.getMainLooper())
    private val callback = Runnable { /* update UI */ }

    override fun onResume() {
        super.onResume()
        mainHandler.postDelayed(callback, 5000)
    }

    override fun onPause() {
        mainHandler.removeCallbacks(callback) // verplicht: wachtrij leegmaken
        super.onPause()
    }
}

Inner Classes — een niet-statische innerlijke klasse heeft een impliciete referentie naar de instantie van de buitenste klasse. Als de buitenste klasse Activity is en de innerlijke klasse ergens buiten is doorgegeven (bijv. aan RecyclerView.Adapter), kan Activity niet worden verzameld.

  • TimerTask en ScheduledExecutorService — taken die zijn gepland vóór vernietiging van Activity
  • BroadcastReceiver — niet geregistreerd in onPause/onDestroy blijft Context vasthouden
  • ViewModel met referentie naar View — ViewModel overleeft Activity, referentie naar View leidt tot lek
  • Retrofit Call — als Call niet is geannuleerd, komt het antwoord aan in vernietigde Fragment

Diagnostische hulpmiddelen voor geheugenlekken

Android Studio Memory Profiler — het ingebouwde hulpmiddel voor real-time monitoring van de heap. Toont een grafiek van bezet geheugen, het aantal allocaties en objecten per type. Maakt het mogelijk een heap dump op te nemen en te exporteren naar HPROF-formaat voor analyse in MAT.

Eclipse MAT (Memory Analyzer Tool) — desktopanalysator voor heap dumps. Bouwt automatisch een Leak Suspects Report dat objecten met de grootste retained size markeert en een mogelijke GC-rootketen voorstelt voor elk verdacht object.

Xcode Memory Graph Debugger — voor iOS. Onderbreekt de applicatie en visualiseert de objectgraaf. Retain cycles worden rood gemarkeerd, men kan op elk object klikken om de retain count en referenties te zien.

HulpmiddelMogelijkhedenComplexiteit
Memory ProfilerReal-time grafiek, heap dump, objecttoewijzing bijhoudenLaag
Eclipse MATDominator-boom, lekverdachten, OQL-query'sGemiddeld
LeakCanaryAutomatische detectie, lekspoor in meldingMinimaal
Xcode Memory GraphVisuele graaf van retain cycles, lijst van levende objectenLaag

Volgens de Uber Engineering Blog vermindert het implementeren van automatische geheugenprofilering (LeakCanary + heap dump-analyse) in de CI/CD-pijplijn het aantal geheugengerelateerde incidenten in productie met 60% binnen 3 maanden.

Methoden voor het verhelpen van lekken

Context vervangen — als een object langer leeft dan Activity, gebruik dan applicationContext. Alle langlevende objecten (singletons, repositories, database helpers) moeten Application Context krijgen, niet Activity Context. Uitzondering: UI-componenten die toegang nodig hebben tot thema of resources die specifiek zijn voor Activity.

Lifecycle-aware componenten — het gebruik van LifecycleObserver, DefaultLifecycleObserver of reactivex annuleert automatisch abonnementen bij onDestroy. Android Jetpack biedt lifecycleScope en viewModelScope, die worden opgeruimd door de bijbehorende gebeurtenis.

Static inner class — als de innerlijke klasse geen toegang nodig heeft tot velden van de buitenste klasse, maak hem dan static. Een statische innerlijke klasse heeft geen impliciete referentie naar de buitenste klasse. Als toegang nodig is, gebruik dan WeakReference voor een expliciete referentie.

kotlin
class MyActivity : AppCompatActivity() {

    // ❌ Niet-statische innerlijke klasse — impliciete referentie naar MyActivity
    inner class BadListener : SomeListener {
        override fun onEvent() { /*...*/ }
    }

    // ✅ Statische innerlijke klasse — geen impliciete referentie
    class GoodListener(private val activityRef: WeakReference<MyActivity>) : SomeListener {
        override fun onEvent() { /*...*/ }
    }
}

In iOS gebruik je capture lists: [weak self] in closures die de maker kunnen overleven. Voor delegates gebruik je zwakke referenties (weak var delegate). Voor closures die gegarandeerd alleen tijdens het leven van self worden aangeroepen, kan [unowned self] worden gebruikt, maar voorzichtig — toegang tot een vrijgegeven object veroorzaakt een crash.

Veelgestelde vragen

Hoe vind ik een lek zonder speciale hulpmiddelen?

Voer in Android een paar overgangen tussen schermen uit (Activity A → B → A → B) en controleer adb shell dumpsys meminfo package_name. Als Total PSS gestaag groeit en niet terugkeert naar de oorspronkelijke waarde — is er een lek. In iOS gebruik je op dezelfde manier Debug Memory Graph in Xcode voor visuele controle.

Kan een Kotlin-coroutine een lek veroorzaken?

Ja, als CoroutineScope niet wordt geannuleerd bij vernietiging van de component. Een coroutine die is gestart in GlobalScope blijft uitvoeren, zelfs na finish() van Activity. Oplossing: gebruik viewModelScope (geannuleerd in onCleared) of lifecycleScope (geannuleerd in onDestroy). Voor eigen Scopes maak je lifecycle-aware scopes via LifecycleOwner.

Hoe beïnvloedt Bitmap lekken?

Bitmap slaat pixelgegevens op in native heap, niet in Java heap. Dit betekent dat Java GC de werkelijke grootte van Bitmap niet ziet. Als Bitmap niet wordt gerecycled met recycle() of de referentie niet wordt gereset, wordt native geheugen niet vrijgegeven. Gebruik BitmapFactory met inSampleSize voor het laden van verkleinde kopieën en Glide/Coil voor automatisch cachebeheer.

Wat is een lek via een statisch veld?

Statisch veld — is een GC Root. Het leeft zolang de klasse is geladen (in Android — zolang Process leeft). Als een statisch veld verwijst naar Activity, Bitmap, View of een ander zwaar object, wordt dit object nooit door GC verzameld. Een statisch veld is een eeuwige referentie. Oplossing: bewaar alleen WeakReference of reset het statische veld in onDestroy.

Hoe voorkom ik lekken in iOS met ARC?

ARC maakt automatisch objecten vrij wanneer de teller van sterke referenties naar nul daalt. Retain cycle — de enige manier van lekken bij ARC. Gebruik altijd weak voor parent→child-referenties, waar child de parent moet overleven (delegates, data source). Voor closures gebruik je capture list [weak self] en controleer je self op nil binnen de closure.

Samenvatting

  • Geheugenlek — object ontoegankelijk voor code, maar niet verwijderd door GC omdat er een actieve referentie van GC Root is
  • GC Roots omvatten statische velden, stapelvariabelen en JNI-referenties; elk object dat ervan bereikbaar is, leeft
  • Context lek — het meest voorkomende probleem in Android: doorgeven van Activity Context aan singleton of statisch veld
  • Handler en Inner Class — de tweede meest voorkomende oorzaak: niet-geannuleerde berichten in de Looper-wachtrij houden referentie naar Activity
  • LeakCanary — het standaard hulpmiddel voor automatische detectie; maakt heap dump en toont de exacte GC-rootketen
  • lifecycleScope en viewModelScope lossen het lekprobleem via coroutines op — automatische annulering bij destroy
  • Profileer geheugen in CI/CD: LeakCanary in debug + heap dump-analyse in testrun moeten merge blokkeren bij nieuwe lekken

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