Java- und Kotlin-Anwendungen verwalten den Speicher über einen Heap mit automatischer Speicherbereinigung. Wenn Objekte nicht mehr erreichbar sind, gibt der Garbage Collector (GC) ihren Speicherplatz schließlich wieder frei. Speicherlecks treten auf, wenn Objekte, die nicht mehr benötigt werden, weiterhin von „GC-Roots“ gehalten werden, wodurch verhindert wird, dass sie zurückgefordert werden.
Wichtige Konzepte
GC-Roots
Ein GC-Root ist ein spezieller Objekttyp, der vom Garbage Collector immer als erreichbar behandelt wird. Beispiele:
- Aktive Threads (und Objekte, auf die in den aktuell ausgeführten Java-Stackframes verwiesen wird).
- Klassen mit aktiv ausgeführten Methoden.
- JNI-Referenzen (globale oder lokale Referenzen, die vom nativen Code gehalten werden).
Pfad zum GC-Stamm
Solange es eine Kette von Referenzen von einem GC-Root zu einem Objekt gibt, ist dieses Objekt „erreichbar“ und kann nicht per Garbage Collection entfernt werden. Diese Kette wird als Pfad zum GC-Root bezeichnet. Um ein Speicherleck zu beheben, müssen Sie diese Kette identifizieren und unterbrechen.

Dominator-Bäume
Der Pfad zum GC-Root gibt zwar an, warum ein Objekt aktiv ist, aber nicht, wie viel Arbeitsspeicher freigegeben würde, wenn diese Referenz unterbrochen würde. Dazu verwenden wir Dominator Trees.
Objekt A dominiert Objekt B, wenn jeder Pfad von einem beliebigen GC-Stamm zu B über A verlaufen muss. Wenn A B dominiert, kann B auch zurückgefordert werden, wenn A zurückgefordert wird, da es keine anderen Pfade von einem beliebigen Stamm zu B gibt.
Das folgende Diagramm zeigt einen Objektgraphen und den entsprechenden Dominatorbaum. Beachten Sie, dass das Objekt D sowohl von A als auch von B im Diagramm erreicht wird. Daher dominiert weder A noch B D. Stattdessen ist der GC-Root der nächste Dominator.

Java-Heap-Dumps abrufen
Ein Heap-Dump ist eine Momentaufnahme aller Objekte im Java-Heap zu einem bestimmten Zeitpunkt.
ADB verwenden
Wenn Sie einen Heap-Dump eines laufenden Prozesses erfassen möchten, können Sie den Paketnamen direkt an am dumpheap übergeben. Damit Sie diesen Befehl ausführen können, müssen Sie Ihre App mit <profileable android:shell="true"/> oder <debuggable> erstellen.
# 1. Trigger the dump (the command takes a moment to complete):
adb shell am dumpheap -g -b png com.android.memorylab /data/local/tmp/heap.hprof
# 2. Pull the file to your development machine:
adb pull /data/local/tmp/heap.hprof .
Perfetto verwenden
Mit Perfetto können auch Java-Heap-Dumps als Teil eines systemweiten Traces erfasst werden. Dazu müssen Sie die Datenquelle android.java_hprof in Ihrer Perfetto-Konfiguration aktivieren. Das ist nützlich, um den Heap-Status mit anderen Systemereignissen in Beziehung zu setzen.
Mit dem folgenden Befehl können Sie mit Perfetto einen Heap-Dump für die MemoryLab-App erfassen:
# Create a temp file for the configuration
cat > /tmp/java_heap.pbtx <<EOF
data_sources: {
config {
name: "android.java_hprof"
java_hprof_config {
process_cmdline: "com.android.memorylab"
}
}
}
EOF
# Run trace command referencing the file
external/perfetto/tools/record_android_trace -o java_heap.perfetto-trace \
-t 10s -c /tmp/java_heap.pbtx
Weitere Informationen finden Sie in der Perfetto-Dokumentation zu Java-Heap-Dumps.
Mit AHAT analysieren
AHAT (Android Heap Analysis Tool) ist das empfohlene Tool zum Anzeigen von .hprof-Dateien in einem Webbrowser.
AHAT wird gestartet
Wenn Sie ahat in Ihrem Pfad installiert haben, starten Sie es mit:
ahat heap.hprof
Oder führen Sie die eigenständige JAR-Datei aus:
java -jar ahat.jar heap.hprof
Öffnen Sie dann Ihren Browser und rufen Sie http://localhost:7100 auf.
Weitere Informationen zum Abrufen oder Erstellen von AHAT finden Sie im AHAT-Quell-Repository.
Wichtige Analyse-Workflows
Lecks finden
Suchen Sie in der Ansicht Zuweisungen nach Ihrer Aktivitätsklasse (MainActivity).

Klicken Sie auf den Kurs, um alle Instanzen zu sehen.
Klicken Sie auf die MainActivity-Instanz, um sie zu untersuchen.

In der Instanzansicht finden Sie den Beispielpfad vom GC-Root, der die Kette von Referenzen zeigt, die verhindern, dass das Objekt per Garbage Collection entfernt wird, sowie die Objektgröße, die angibt, wie viel Speicher von dieser bestimmten Instanz belegt wird.

Bitmaps analysieren
AHAT bietet spezielle Unterstützung für das Ansehen von android.graphics.Bitmap-Objekten, die oft viel Speicherplatz benötigen. Klicken Sie auf eine Bitmap-Instanz, um eine gerenderte Vorschau des Inhalts aufzurufen.

Seite „Aktivitätslecks“
AHAT bietet eine spezielle Ansicht zum Identifizieren von Speicherlecks bei Aktivitäten, die zu den häufigsten und schwerwiegendsten Speicherlecks in Android gehören.
- Aktion: Tippen Sie in MemoryLab auf Leak an Activity (Aktivität durchsickern lassen). Dadurch wird
LeakedActivitygestartet, das sich absichtlich selbst preisgibt. - Dump: Erstellen Sie einen Heap-Dump.
- Analysieren: Klicken Sie in der AHAT-Seitenleiste auf Activity Leaks (Aktivitätslecks).
- Bestätigen: AHAT listet
com.android.memorylab.LeakedActivityals Speicherleck auf, weil das FeldmDestroyedauf „true“ gesetzt ist (was darauf hinweist, dass der Aktivitätslebenszyklus beendet ist), aber es ist immer noch über einen GC-Root erreichbar.

Heap-Dumps vergleichen
Der Vergleich von zwei Heap-Dumps ist eine der effektivsten Methoden, um Speicherprobleme zu erkennen. Wenn Sie einen „sauberen“ Baseline-Dump mit einem Dump vergleichen, der nach einigen Aktionen erstellt wurde, können Sie sofort sehen, welche Objekte hinzugekommen sind.
Übung: Lecks durch Differenzierung erkennen
Baseline: Starten Sie MemoryLab und erstellen Sie einen Baseline-Heap-Dump:
adb shell am dumpheap com.android.memorylab /data/local/tmp/base.hprof adb pull /data/local/tmp/base.hprof .Aktion: Tippen Sie in der App mehrmals auf Java-Speicher zuweisen(10 MB).
Final: Erstellen Sie einen zweiten Heap-Dump:
adb shell am dumpheap com.android.memorylab /data/local/tmp/leaked.hprof adb pull /data/local/tmp/leaked.hprof .Vergleichen: Starten Sie AHAT mit dem zweiten Dump als primären Dump und dem ersten als Baseline:
java -jar out/host/linux-x86/framework/ahat.jar leaked.hprof --baseline base.hprofÜbersicht der Analyse: Die Seite Übersicht enthält jetzt die Spalte Δ (Delta). Sie sehen ein großes positives Delta für den
app-Heap, was auf einen erheblichen Speicherzuwachs hindeutet.

- Aufschlüsseln: Klicken Sie im Menü auf Gerät mit Root-Zugriff. Auf dieser Seite werden Objekte angezeigt, die über GC-Roots erreichbar sind, sortiert nach ihrer beibehaltenen Größe. Oben sehen Sie
MainActivitymit einem großen positiven Delta.

Zuweisungs-Stacktraces aufzeichnen
Der Beispielpfad vom GC-Root gibt zwar an, warum ein Objekt noch vorhanden ist, aber nicht, wie es erstellt wurde. Zuweisungs-Stacktraces enthalten die genaue Codezeile, in der ein Objekt zugewiesen wurde.
Konzept und Kompromisse: Das Aufzeichnen des Stack-Trace jeder Zuweisung ist rechenintensiv und verbraucht viel Arbeitsspeicher. In einer großen Produktions-App kann dies dazu führen, dass die App fast unbrauchbar wird. MemoryLab ist jedoch eine kleine Anwendung, sodass wir dieses Tracking problemlos aktivieren können, um die Quelle der Zuweisungen zu ermitteln.
Übung: Quelle von Byte-Arrays ermitteln
Mit Tracking beginnen: Erzwingen Sie das Beenden von MemoryLab und starten Sie die App mit dem Flag
--track-allocationneu. Erhöhen Sie die Standard-Stapeltiefe, um mehr Kontext zu erfassen.# Increase the allocation tracker's stack depth (requires a process restart) adb shell setprop dalvik.vm.allocTrackerMaxStack 16 adb shell am force-stop com.android.memorylab adb shell am start --track-allocation -n com.android.memorylab/.MainActivityAktion: Tippen Sie mehrmals auf Java-Arbeitsspeicher zuweisen(10 MB).
Dump: Erfassen Sie einen Heap-Dump und ziehen Sie ihn.
Analysieren: Öffnen Sie den Dump in AHAT. Rufen Sie eine große
byte[]-Instanz auf. Beispiel:MainActivity→mJavaAllocations(ArrayList) →elementData(Object[]) → Arrayelement[0].Bestätigen: Sehen Sie sich in der Instanzansicht den Abschnitt Zuweisungswebsite an. Es wird der vollständige Stacktrace angezeigt, der zu
MainActivity.allocateJavaführt.

Doppelte Strings und Hydrations-Bloat erkennen
Auch wenn eine App keine klassischen GC-Root-Leaks aufweist, kann ihr Live-Java-Heap durch Tausende von doppelten java.lang.String-Instanzen aufgebläht sein, die während der Deserialisierung von JSON, Protobuf, Cursor oder Room-Datenbanken erstellt wurden. Wiederholte Schlüssel, Statusstrings, Kategorielabels oder URLs werden oft bei jeder Netzwerkantwort oder Datenbankabfrage neu zugewiesen. In großen Feed-, Messaging- und Content-Apps machen doppelte Strings in der Regel 30% bis 60% des aktiven String-Arbeitsspeichers aus.
So prüfen Sie doppelte Strings in AHAT:
- Öffnen Sie die Seite Zuweisungen und filtern Sie nach
java.lang.String. - Wenn Sie zwei Heap-Dumps mit
--baselinevergleichen, prüfen Sie, ob die Anzahl derjava.lang.String-Instanzen und die Gesamtzahl der Byte nach dem Hydrieren eines Feeds oder dem Laden eines lokalen Cache unverhältnismäßig ansteigen. - Sehen Sie sich die Tabelle mit den
java.lang.String-Instanzen an (sortiert nach Größe oder Wert), um identische Stringwerte zu finden, die in mehreren In-Memory-Modellobjekten beibehalten werden.
- Abhilfe: Rufen Sie
String.intern()nicht wahllos für beliebige Nutzer- oder Netzwerkeingaben auf, da die interne Laufzeittabelle global ist und zu Sperrkonflikten führen oder Strings länger als nötig beibehalten kann. Deduplizieren Sie stattdessen Domain-Strings mit hoher Häufigkeit während der Deserialisierung mithilfe eines begrenzten, bereichsbezogenen Deduplizierungscache (z. B. einLruCache<String, String>in Ihrem Parser oder Adapter) oder stellen Sie feste Wertmengen als Enums oder Ganzzahlkonstanten dar.
Java-Speicherdynamik analysieren (kombiniertes Profil)
Um einen umfassenden Überblick über das Speicherverhalten einer Anwendung zu erhalten, können Sie Speicherzähler, Threadaktivität und callstackbasiertes Zuweisungsprofiling in einem einzigen Perfetto-Trace kombinieren. So können Sie systemweite Speichermesswerte (z. B. RSS und Heap-Größe) mit bestimmten Code-Ausführungs- und Zuweisungspunkten in Beziehung setzen.
Wir verwenden eine kombinierte Konfiguration, die Folgendes ermöglicht:
- Arbeitsspeicherzähler (
linux.process_stats): Ruft RSS- und andere Arbeitsspeicherstatistiken ab. - ATrace (
dalvik-,memory- undsched-Kategorien): Erfasst Thread-Status und GC-Ereignisse. - Heapprofd (
android.heapprofd): Richtet sich sowohl aufcom.android.art- (Java) als auch auflibc.malloc-Heaps (nativ) mit kontinuierlichen Dumps alle 5 Sekunden.
Übung: Kombinierte Memory-Analyse
In dieser Übung führen wir die MemoryLab-App aus und führen eine Reihe von Speichervorgängen durch, um verschiedene Muster im Trace zu beobachten:
- Baseline: Inaktiv
- Java Churn: Temporäre Zuweisungen, die sofort per Garbage Collection entfernt werden.
- Persistent Java Allocation (Persistente Java-Zuweisung): Zuweisen von Java-Objekten, die im Arbeitsspeicher verbleiben.
- Bitmap-Zuweisung: Zuweisen großer Grafik-Assets (die sich im nativen Heap-/Grafikspeicher befinden).
- Rückforderung: Freigabe aller zugewiesenen Ressourcen.
1. Einführung und Vorbereitung
Erzwingen Sie das Beenden der App und starten Sie sie neu, um einen sauberen Zustand zu gewährleisten:
adb shell am force-stop com.android.memorylab adb shell am start -W -n com.android.memorylab/.MainActivity
2. Tracing starten und Sequenz auslösen
Wir starten einen 40-Sekunden-Trace und lösen die Speicherereignisse mit am
broadcast-Befehlen aus.
Trace starten:
adb shell perfetto -c - --txt -o /data/misc/perfetto-traces/java_memory.perfetto-trace <<EOF buffers: { size_kb: 131072 fill_policy: RING_BUFFER } data_sources: { config { name: "linux.process_stats" target_buffer: 0 process_stats_config { scan_all_processes_on_start: true proc_stats_poll_ms: 100 } } } data_sources: { config { name: "linux.ftrace" target_buffer: 0 ftrace_config { ftrace_events: "sched/sched_switch" ftrace_events: "task/task_newtask" ftrace_events: "task/task_rename" ftrace_events: "ftrace/print" atrace_categories: "dalvik" atrace_categories: "am" atrace_categories: "res" atrace_categories: "memory" atrace_categories: "sched" atrace_apps: "com.android.memorylab" } } } data_sources: { config { name: "android.heapprofd" target_buffer: 0 heapprofd_config { sampling_interval_bytes: 4096 process_cmdline: "com.android.memorylab" heaps: "libc.malloc" heaps: "com.android.art" shmem_size_bytes: 8388608 block_client: true continuous_dump_config { dump_phase_ms: 1000 dump_interval_ms: 5000 } } } } duration_ms: 40000 EOFSequenz auslösen: Führen Sie diese Befehle im Hostterminal aus, während der Trace ausgeführt wird. Beachten Sie dabei die vorgeschlagene Zeitplanung:
# Wait ~5s for trace initialization, then start Java churn: adb shell am broadcast -a com.android.memorylab.CHURN_JAVA # Wait ~10s (at 15s mark), allocate 10MB of persistent Java memory: adb shell am broadcast -a com.android.memorylab.ALLOC_JAVA # Wait ~5s (at 20s mark), allocate 20MB of Bitmaps (native/graphics): adb shell am broadcast -a com.android.memorylab.LEAK_BITMAP # Wait ~10s (at 30s mark), free everything: adb shell am broadcast -a com.android.memorylab.FREE_ALLAlternative (CLI-Tool): Sie können das Profil auch direkt mit dem
heap_profile-Skript starten und sowohl Java- als auch native Heaps mit kontinuierlichen Dumps erfassen:external/perfetto/tools/heap_profile -n com.android.memorylab \ --heaps com.android.art,libc.malloc \ -c 5000 \ -d 40000 \ -o java_memory_profile
3. Kombinierten Trace analysieren
Öffnen Sie die erfassten java_memory.perfetto-trace in der Perfetto-Benutzeroberfläche.
Wichtige Tracks in Perfetto
Bevor Sie die Zeitachse analysieren, suchen Sie nach diesen wichtigen Tracks für den com.android.memorylab-Prozess:
mem.rss.anon(Anonymer RSS): Im Bereich Arbeitsspeicher des Prozesses. In diesem Track wird der physische Arbeitsspeicher (RAM) gemessen, der dem Prozess vom Betriebssystem zugewiesen wird. Sie stellt den tatsächlichen Speicherbedarf dar.Heap size (KB): Auch im Bereich Arbeitsspeicher. Dies ist ein Dalvik-/ART-spezifischer Zähler, der den für den Java-Heap reservierten virtuellen Adressraum darstellt. Sie gibt das interne Heap-Limit der VM an, das sich ändert, wenn Objekte zugewiesen und Garbage Collection ausgeführt wird.HeapTaskDaemon: In der Liste der Threads unter dem Prozess. Dies ist der Hintergrundthread, in dem der ART Garbage Collector die meiste Arbeit verrichtet. „Aktivität“ bezieht sich hier auf aktive GC-Gutscheine.- Kontinuierliche Zuweisungs-Dumps (heapprofd): Werden als farbige Abschnitte entlang der oberen Zeitachse dargestellt. Jeder Abschnitt steht für einen Zeitraum. Wenn Sie auf ein Segment klicken oder einen Zeitraum auswählen, können Sie das Flammendiagramm (im unteren Bereich) für com.android.art (Java-Zuweisungen) oder libc.malloc (native Zuweisungen) untersuchen, um zu sehen, was in diesem Zeitraum zugewiesen wurde.
Analyse chronologischer Phasen
Sehen wir uns den Trace chronologisch an, um zu sehen, wie diese Tracks in den einzelnen Phasen des Trainings interagieren.
Phase 1: Baseline (0–5 Sekunden)
- Was passiert? Die App ist inaktiv und wartet auf Befehle.
- Status des Tracks:
mem.rss.anon: Flache Linie auf der Baseline (in der Regel etwa 60–80 MB, je nach Gerät).Heap size (KB): Flache Linie, die der anfänglichen Java-Heap-Zuweisung entspricht.HeapTaskDaemon: Inaktiv (keine Slices, die die Ausführung zeigen).- Zuweisungs-Dumps: Hier werden minimale Baseline-Zuweisungen angezeigt.

Phase 2: Java-Zuweisungs-Churn (5–15 Sekunden)
- Was passiert?: Der
AllocationChurnThreadwird gestartet und es werden wiederholt 1-MB-Arrays zugewiesen und verworfen. - Status des Tracks:
Heap size (KB): Zeigt ein schnelles Sägezahnmuster. Die Heap-Größe steigt mit zunehmenden Zuweisungen und sinkt scharf, wenn die Garbage Collection ausgeführt wird.HeapTaskDaemon: Zeigt eine nahezu konstante Aktivität, wobei die Ausführungsslice perfekt mit den Einbrüchen imHeap size-Sägezahnmuster übereinstimmen.mem.rss.anon: Erfasst die Java-Heap-Aktivität.- Java-Heap-Zuweisungs-Dumps: Wenn Sie Segmente in diesem Track auswählen, werden Heap-Zuweisungen für com.android.art angezeigt.
Die Zuweisungsbeispiele zeigen AllocationChurnThread als primären Zuweiser. Alle Zuweisungen haben denselben Callstack, der auf die Lambda-Funktion in MainActivity.java verweist.

Phase 3: Persistente Java-Zuweisung (15–20 Sekunden)
- Was passiert?: Wir weisen 10 MB an Java-Objekten zu und behalten einen Verweis darauf in
mJavaAllocationsbei. - Status des Tracks:
Heap size (KB): Die Baseline der Sägezahnschritte steigt um etwa 10 MB.mem.rss.anon: Die Größe steigt um etwa 10 MB, da das Betriebssystem diese dauerhafte Zuweisung mit neuen physischen Seiten sichern muss.- Java-Heap-Zuweisungs-Dumps: Wenn Sie Segmente in diesem Track auswählen, werden Heap-Zuweisungen für com.android.art angezeigt.
- Zuweisungs-Dumps (Flamegraph): Wenn Sie den Heap com.android.art für den in diesem Fenster erstellten Dump untersuchen, sehen Sie einen neuen Zuweisungspfad von
MainActivity.allocateJava, der zur beibehaltenen Größe beiträgt.
Wählen Sie eine Zuweisungsstichprobe aus, deren Zeitraum sich mit dem Zeitraum der Erhöhung um 10 MB für die dauerhafte Zuweisung überschneidet. Die Zuweisungs-Callstacks sollten sich in zwei verschiedene Websites aufteilen. Eine ist für die kurzlebige Zuweisung verantwortlich, die wir bereits gesehen haben, und die andere für die neue, langlebige Zuweisung.

Phase 4: Bitmap-Zuweisung (20–30 Sekunden)
- Was passiert?: Wir weisen auch 20 MB für Bitmaps zu.
- Status des Tracks:
Heap size (KB): Wie zuvor.mem.rss.anon: Hier ist ein deutlicher Anstieg von etwa 20 MB zu sehen, der den nativen Zuweisungen für die Bitmap-Pixeldaten entspricht.- Speicherzuweisungs-Dumps (Flamegraph): Konzentrieren Sie sich diesmal auf die Slices für den Heap libc.malloc (nativ).
Die Callstacks für die native Zuweisung zeigen die Bitmap-Zuweisung aus nativen Grafikbibliotheken. Dies ist ein guter Anwendungsfall für die native Nachverfolgung der Arbeitsspeicherzuweisung, da diese Bitmap-Zuweisungen nicht im Java-Heap angezeigt werden.

Phase 5: Rückgewinnung (30–40 Sekunden)
- Was passiert?: Wir lösen
FREE_ALLaus, wodurch Verweise auf alle persistenten Java-Zuweisungen und Bitmaps gelöscht werden. Danach erfolgt ein explizitesSystem.gc(). - Status des Tracks:
Heap size (KB): Die Leistung sinkt wieder auf das Ausgangsniveau.mem.rss.anon: Sinkt wieder, da das Betriebssystem die physischen Seiten zurückfordert.HeapTaskDaemon: Zeigt einen letzten Aktivitätsschub, während die Garbage Collection verarbeitet wird.

Verlauf von OOM-Fehlern überwachen (ApplicationExitInfo)
Ein LMK während des Auftretens zu erfassen, ist ideal für das aktive Debugging. Für die Feldtelemetrie können Sie jedoch die ApplicationExitInfo API verwenden. So kann Ihre App herausfinden, warum sie in einer vorherigen Sitzung beendet wurde.
ActivityManager am = getSystemService(ActivityManager.class);
List<ApplicationExitInfo> exitReasons = am.getHistoricalProcessExitReasons(null, 0, 1);
if (!exitReasons.isEmpty()) {
ApplicationExitInfo info = exitReasons.get(0);
if (info.getReason() == ApplicationExitInfo.REASON_LOW_MEMORY) {
// App was killed by the system Low Memory Killer
}
}
Best Practices
- Baseline First: Nehmen Sie immer einen Heap-Dump als Baseline auf, nachdem die App initialisiert wurde, aber bevor Sie die Aktion ausführen, die Sie testen.
- AHAT-Seite „Activity Leaks“ verwenden: AHAT enthält eine spezielle Seite Activity Leaks (Activity-Leaks), auf der automatisch Activity-Instanzen identifiziert werden, die zerstört wurden, aber weiterhin im Arbeitsspeicher gehalten werden. Das ist oft der schnellste Weg, um häufige Lecks zu finden.
- Pfad zu GC-Roots prüfen: Verwenden Sie für jedes Objekt, bei dem ein Speicherleck auftritt, die Ansicht Pfad von Root in AHAT, um genau zu verstehen, welche Referenz das Objekt am Leben hält (z.B. ein statisches Feld, ein Thread mit langer Laufzeit oder ein registrierter Listener).
← Tools | ↑ Nach oben | Bitmaps →