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
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.
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.
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.
// 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.
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).
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.
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.
| Hulpmiddel | Mogelijkheden | Complexiteit |
|---|---|---|
| Memory Profiler | Real-time grafiek, heap dump, objecttoewijzing bijhouden | Laag |
| Eclipse MAT | Dominator-boom, lekverdachten, OQL-query's | Gemiddeld |
| LeakCanary | Automatische detectie, lekspoor in melding | Minimaal |
| Xcode Memory Graph | Visuele graaf van retain cycles, lijst van levende objecten | Laag |
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.
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.
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
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.
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.
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.
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.
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
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