Speicherleck (memory leak) — eine Situation, in der eine Anwendung den von Objekten belegten Speicher nicht freigibt, die nicht mehr benötigt werden. In der mobilen Entwicklung ist dies besonders kritisch: begrenzter Heap und fehlender Swap führen zu OutOfMemoryError und Anwendungsabstürzen. Laut Purdue University (2022) enthalten 35% der Android-Apps im Google Play mindestens ein Speicherleck. Lassen Sie uns typische Szenarien, Diagnosetools und Behebungsmethoden betrachten.
Wichtige Punkte
Speicherleck — eine Situation, in der zugewiesener Speicher nicht an das System zurückgegeben wird, nachdem das Objekt vom Programm nicht mehr benötigt wird. Der Garbage Collector betrachtet ein solches Objekt als lebendig, weil eine aktive Referenzkette von einem GC Root darauf zeigt.
In Java/Kotlin arbeitet der Garbage Collector automatisch, kann aber nicht feststellen, dass ein Objekt logisch nicht mehr benötigt wird, wenn eine technische Referenz darauf existiert. Der Entwickler muss unnötige Verbindungen explizit trennen. In Swift/Objective-C zählt ARC automatisch Referenzen, aber Retain Cycles blockieren das Nullwerden des Zählers.
Die Hauptgefahr von Lecks ist der kumulative Effekt. Jedes Leck verbraucht eine kleine Menge Speicher, aber bei wiederholten Bildschirmwechseln (Bildschirmdrehung, Öffnen/Schließen von Activities) sammeln sich Lecks an, bis das Heap-Limit erschöpft ist.
Leck — das Objekt ist für den Code unzugänglich, wird aber nicht vom GC entfernt. Aufblähung — das Objekt ist logisch erforderlich, wird aber in übermäßiger Menge gespeichert. Beispiel für Aufblähung: ein Bildercache von 100 MB bei einem Arbeitssatz von 30 MB. Beide Probleme führen zu OOM, aber die Ursachen und Behandlungsmethoden sind unterschiedlich.
ART (Android Runtime) verwendet generationsbasierte Garbage Collection mit gleichzeitiger Kompaktierung. Der Speicher wird in junge Generation (Young), alte Generation (Old) und große Objekte (Large) unterteilt. Objekte, die mehrere GC-Zyklen überleben, werden in die Old-Generation verschoben, wo die Sammlung seltener erfolgt — dies beschleunigt normale Zyklen.
Der GC startet, wenn der Heap einen bestimmten Auslastungsschwellenwert (normalerweise 75-85%) erreicht. Während des GC werden alle Anwendungsthreads angehalten (STW — Stop The World). Je mehr lebende Objekte, desto länger die Pause. Lecks erhöhen die Anzahl der lebenden Objekte und verlängern die GC-Pausen.
Der Collector bestimmt lebende Objekte, indem er den Graphen von den GC Roots aus durchläuft: statische Felder, Stapelvariablen aktiver Threads, JNI-Referenzen. Jedes Objekt, das über Referenzen von diesen Wurzeln aus erreichbar ist, gilt als lebendig — selbst wenn der Entwickler weiß, dass es nicht mehr benötigt wird.
// Beispiel: statische Sammlung als GC Root — permanentes Leck
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 verhindert GC nicht — korrektes Verhalten
}
}
WeakReference löst das Problem: Der GC ignoriert schwache Referenzen bei der Bestimmung lebender Objekte. Wenn nur noch schwache Referenzen auf ein Objekt verweisen, wird es im nächsten GC-Zyklus eingesammelt.
Activity Context — das massivste Leck-Szenario in Android. Wenn ein Singleton, ein statisches Feld oder ein langlebiger Dienst eine Referenz auf einen Activity Context speichert, kann die gesamte Activity mit allen Views nicht vom GC eingesammelt werden. Lösung: Verwenden Sie Application Context für langlebige Objekte.
Handler und gesendete Nachrichten — Handler.postDelayed(runnable, delay) stellt eine Nachricht in die Warteschlange des Main Looper. Wenn die Activity vor Ablauf der Verzögerung zerstört wird, ist die Nachricht noch in der Warteschlange und hält eine Referenz über Runnable → anonyme Klasse → äußere 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) // erforderlich: Warteschlange leeren
super.onPause()
}
}
Innere Klassen — eine nicht-statische innere Klasse hat eine implizite Referenz auf die Instanz der äußeren Klasse. Wenn die äußere Klasse eine Activity ist und die innere Klasse nach außen übergeben wird (z.B. an RecyclerView.Adapter), kann die Activity nicht eingesammelt werden.
Android Studio Memory Profiler — integriertes Tool für Echtzeit-Heap-Überwachung. Zeigt ein Diagramm des belegten Speichers, die Anzahl der Allokationen und Objekte nach Typ. Ermöglicht die Aufnahme eines Heap Dumps und den Export im HPROF-Format für die Analyse in MAT.
Eclipse MAT (Memory Analyzer Tool) — Desktop-Analyzer für Heap Dumps. Erstellt automatisch Leak Suspects Reports, die Objekte mit der größten retained size hervorheben und für jedes verdächtige Objekt eine vermutete GC-Root-Chain vorschlagen.
Xcode Memory Graph Debugger — für iOS. Hält die Anwendung an und visualisiert den Objektgraphen. Retain Cycles werden rot hervorgehoben; Sie können auf jedes Objekt klicken, um seine Retain Count und Referenzen zu sehen.
| Werkzeug | Funktionen | Komplexität |
|---|---|---|
| Memory Profiler | Echtzeit-Diagramm, Heap Dump, Object Allocation Tracking | Niedrig |
| Eclipse MAT | Dominator Tree, Leak Suspects, OQL-Abfragen | Mittel |
| LeakCanary | Automatische Erkennung, Leck-Trace in Benachrichtigung | Minimal |
| Xcode Memory Graph | Visueller Retain-Cycle-Graph, Live-Objektliste | Niedrig |
Laut Uber Engineering Blog reduziert die Integration von automatischem Speicher-Profiling (LeakCanary + Heap-Dump-Analyse) in die CI/CD-Pipeline speicherbezogene Vorfälle in der Produktion innerhalb von 3 Monaten um 60%.
Context ersetzen — wenn ein Objekt die Activity überlebt, verwenden Sie applicationContext. Alle langlebigen Objekte (Singletons, Repositories, Datenbank-Helfer) sollten Application Context erhalten, nicht Activity Context. Ausnahme: UI-Komponenten, die Zugriff auf das theme oder Activity-spezifische Ressourcen benötigen.
Lifecycle-aware Komponenten — die Verwendung von LifecycleObserver, DefaultLifecycleObserver oder reaktiven Erweiterungen bricht Abonnements automatisch bei onDestroy ab. Android Jetpack bietet lifecycleScope und viewModelScope, die durch das entsprechende Lebenszyklusereignis bereinigt werden.
Statische innere Klasse — wenn die innere Klasse keinen Zugriff auf die Felder der äußeren Klasse benötigt, machen Sie sie static. Eine statische innere Klasse hat keine implizite Referenz auf die äußere Klasse. Wenn Zugriff benötigt wird, verwenden Sie WeakReference für die explizite Referenz.
class MyActivity : AppCompatActivity() {
// ❌ Nicht-statische innere Klasse — implizite Referenz auf MyActivity
inner class BadListener : SomeListener {
override fun onEvent() { /*...*/ }
}
// ✅ Statische innere Klasse — keine implizite Referenz
class GoodListener(private val activityRef: WeakReference<MyActivity>) : SomeListener {
override fun onEvent() { /*...*/ }
}
}
Verwenden Sie in iOS Capture Lists: [weak self] in Closures, die ihren Ersteller überleben können. Für Delegates verwenden Sie schwache Referenzen (weak var delegate). Für Closures, die garantiert nur während der Lebensdauer von self aufgerufen werden, kann [unowned self] verwendet werden, aber mit Vorsicht — der Zugriff auf ein freigegebenes Objekt führt zu einem Absturz.
Häufig gestellte Fragen
Führen Sie in Android mehrere Bildschirmwechsel durch (Activity A → B → A → B) und überprüfen Sie adb shell dumpsys meminfo package_name. Wenn Total PSS stetig wächst und nicht zum ursprünglichen Wert zurückkehrt — liegt ein Leck vor. In iOS ähnlich: Verwenden Sie Debug Memory Graph in Xcode zur visuellen Überprüfung.
Ja, wenn der CoroutineScope bei der Zerstörung der Komponente nicht abgebrochen wird. Eine in GlobalScope gestartete Koroutine wird auch nach finish() der Activity weiter ausgeführt. Lösung: Verwenden Sie viewModelScope (wird in onCleared abgebrochen) oder lifecycleScope (wird in onDestroy abgebrochen). Für benutzerdefinierte Scopes erstellen Sie lifecycle-aware Scopes über LifecycleOwner.
Bitmap speichert Pixeldaten im nativen Heap, nicht im Java-Heap. Das bedeutet, dass der Java-GC die tatsächliche Größe des Bitmap nicht sieht. Wenn Bitmap nicht mit recycle() aufgerufen oder die Referenz nicht auf null gesetzt wird, wird der native Speicher nicht freigegeben. Verwenden Sie BitmapFactory mit inSampleSize zum Laden verkleinerter Kopien und Glide/Coil für automatische Cache-Verwaltung.
Ein statisches Feld ist ein GC Root. Es lebt, solange die Klasse geladen ist (in Android — solange der Process lebt). Wenn ein statisches Feld auf eine Activity, Bitmap, View oder ein anderes schweres Objekt verweist, wird dieses Objekt niemals vom GC eingesammelt. Ein statisches Feld ist eine ewige Referenz. Lösung: Speichern Sie nur WeakReference oder setzen Sie das statische Feld in onDestroy auf null.
ARC gibt Objekte automatisch frei, wenn der starke Referenzzähler auf Null fällt. Ein Retain Cycle ist die einzige Möglichkeit für ein Leck unter ARC. Verwenden Sie immer weak für Eltern→Kind-Referenzen, bei denen das Kind den Elternteil überleben kann (Delegates, Data Sources). Für Closures verwenden Sie die Capture List [weak self] und prüfen Sie self innerhalb der Closure auf nil.
Zusammenfassung
Wir entwickeln eine mobile Applikation schlüsselfertig
IT Sectr entwickelt seit 2017 iOS- und Android-Apps für Startups und Unternehmen. Wir beraten Sie und schlagen die beste Lösung vor.
Lesen Sie auch