Alles, was Sie über Speicherlecks in Android wissen müssen.

Ali Asadi
Jun 30, 2019 – 16 min read

Einer der Kernvorteile von Java, oder genauer gesagt, der JVM (Java Virtual Machine), ist der Garbage Collector (GC). Wir können neue Objekte erstellen, ohne uns Gedanken darüber machen zu müssen, wie wir sie aus dem Speicher befreien. Der Garbage Collector kümmert sich um die Zuweisung und Freigabe des Speichers für uns.

Nicht ganz! Wir können den Garbage Collector daran hindern, den Speicher für uns freizugeben, wenn wir nicht vollständig verstehen, wie der GC funktioniert.

Wenn wir einen Code schreiben, ohne gut zu verstehen, wie der GC funktioniert, kann es zu Speicherlecks in der Anwendung kommen. Diese Lecks können sich auf unsere Anwendung auswirken, indem sie nicht freigegebenen Speicher verschwenden und schließlich zu „Out of Memory“-Ausnahmen und Verzögerungen führen.

Was ist ein Speicherleck?

Fehler bei der Freigabe von unbenutzten Objekten aus dem Speicher

Fehler bei der Freigabe von unbenutzten Objekten aus dem Speicher bedeutet, dass es unbenutzte Objekte in der Anwendung gibt, die der GC nicht aus dem Speicher löschen kann.

Wenn der GC die unbenutzten Objekte nicht aus dem Speicher löschen kann, sind wir in Schwierigkeiten. Die Speichereinheit, die die unbenutzten Objekte enthält, wird bis zum Ende der Anwendung oder (bis zum Ende der Methode) belegt sein.

Bis zum Ende der Methode? Ja, das ist richtig. Wir haben zwei Arten von Lecks, Lecks, die die Speichereinheit bis zum Ende der Anwendung belegen und Lecks, die die Speichereinheit bis zum Ende der Methode belegen. Die erste Art ist klar. Die zweite bedarf einer genaueren Erläuterung. Nehmen wir ein Beispiel, um dies zu erklären! Nehmen wir an, wir haben eine Methode X. Methode X führt im Hintergrund eine langwierige Aufgabe aus, deren Beendigung eine Minute dauern wird. Außerdem hält Methode X während dieser Aufgabe unbenutzte Objekte. In diesem Fall wird die Speichereinheit belegt, und die unbenutzten Objekte können eine Minute lang bis zum Ende der Aufgabe nicht gelöscht werden. Nach Beendigung der Methode kann der GC die unbenutzten Objekte löschen und den Speicher zurückfordern.

Das ist es, was ich euch für den Moment wissen lassen möchte, wir werden später mit etwas Code und Visualisierung darauf zurückkommen. DAS WIRD LUSTIG. 👹😜

Warte mal!!!🥴

Bevor wir zum Kontext springen, lass uns mit den Grundlagen beginnen.

RAM, oder Random Access Memory, ist der Speicher in Android-Geräten oder Computern, der verwendet wird, um aktuell laufende Anwendungen und deren Daten zu speichern.

Ich werde zwei Hauptfiguren im RAM erklären, die erste ist der Heap, und die zweite ist der Stack. Kommen wir zum lustigen Teil 🤩🍻.

Heap & Stack

Ich werde es nicht zu lang machen. Eine kurze Beschreibung: Der Stack wird für die statische Speicherzuweisung verwendet, während der Heap für die dynamische Speicherzuweisung verwendet wird. Vergessen Sie nicht, dass sowohl der Heap als auch der Stack im RAM gespeichert werden.

Mehr über den Heap-Speicher

Der Heap-Speicher in Java wird von der virtuellen Maschine verwendet, um Objekte zuzuweisen. Wenn Sie ein Objekt erstellen, wird es immer im Heap-Speicher erstellt. Virtuelle Maschinen wie JVM oder DVM führen regelmäßig eine Garbage Collection (GC) durch und machen den Heap-Speicher aller Objekte, die nicht mehr referenziert werden, für zukünftige Zuweisungen verfügbar.

Um eine reibungslose Benutzererfahrung zu gewährleisten, setzt Android eine harte Grenze für die Heap-Größe für jede laufende Anwendung. Die Begrenzung der Heap-Größe variiert von Gerät zu Gerät und basiert darauf, wie viel RAM ein Gerät hat. Wenn Ihre Anwendung dieses Heap-Limit erreicht und versucht, mehr Speicher zuzuweisen, erhält sie eine OutOfMemoryError und wird beendet.

Haben Sie sich schon einmal gefragt, wie groß der Heap Ihrer Anwendung ist?

Lassen Sie uns dies gemeinsam herausfinden. In Android haben wir die Dalvik VM (DVM). Die DVM ist eine einzigartige Java Virtual Machine, die für mobile Geräte optimiert ist. Sie optimiert die virtuelle Maschine in Bezug auf Speicher, Akkulaufzeit und Leistung und ist für die Verteilung der Speichermenge für jede Anwendung verantwortlich.

Lassen Sie uns über zwei Zeilen in der DVM sprechen:

  1. dalvik.vm.heapgrowthlimit: Diese Zeile basiert darauf, wie Dalvik mit der Heap-Größe Ihrer Anwendung beginnen wird. Es ist die Standard-Heapgröße für jede Anwendung. Das Maximum, das Ihre Anwendung erreichen kann!
  2. dalvik.vm.heapsize: Diese Zeile stellt die maximale Heap-Größe für einen größeren Heap dar. Sie können das erreichen, indem Sie android um einen größeren Heap in Ihrem Anwendungsmanifest bitten (android:largeHeap=“true“).

Verwenden Sie keinen größeren Heap in Ihrer Anwendung. Tun Sie das NUR, wenn Sie die Nebenwirkung dieses Schrittes genau kennen. Hier gebe ich Ihnen genug Informationen, um das Thema weiter zu erforschen.

Hier ist eine Tabelle, die zeigt, welche Heap-Größe Sie basierend auf Ihrem Geräte-RAM haben:

+==========================+=========+=========+===================+
| DVM | 1GB RAM | 2GB RAM | 3GB RAM OR HIGHER |
+==========================+=========+=========+===================+
| DEFAULT(heapgrowthlimit) | 64m | 128m | 256m |
+--------------------------+---------+---------+-------------------+
| LARGE(heapsize) | 128m | 256m | 512m |
+--------------------------+---------+---------+-------------------+

Erinnern Sie sich, je mehr Ram Sie haben, desto höher wird die Heap-Größe sein. Denken Sie daran, dass nicht alle Geräte mit höherem RAM über 512m gehen, recherchieren Sie auf Ihrem Gerät, wenn Ihr Gerät mehr als 3GB hat, um zu sehen, ob Ihre Heap-Größe größer als 512m ist.

Wie können Sie die App Heap-Größe für Ihr Gerät überprüfen?

Verwenden Sie den ActivityManager. Sie können die maximale Heap-Größe zur Laufzeit überprüfen, indem Sie die Methoden getMemoryClass() oder getLargeMemoryClass() (wenn ein großer Heap aktiviert ist) verwenden.

  • getMemoryClass(): Gibt die standardmäßige maximale Heap-Größe zurück.
  • getLargeMemoryClass(): Gibt die maximal verfügbare Heap-Größe zurück, nachdem das Large Heap Flag im Manifest aktiviert wurde.
ActivityManager am = getSystemService(ACTIVITY_SERVICE);
Log.d("XXX", "dalvik.vm.heapgrowthlimit: " + am.getMemoryClass());
Log.d("XXX", "dalvik.vm.heapsize: " + am.getLargeMemoryClass());

Wie funktioniert das in der realen Welt?

Wir werden diese einfache Anwendung verwenden, um zu verstehen, wann wir den Heap und wann wir den Stack verwenden.

Anwendung

Das folgende Bild zeigt eine Darstellung des Heaps und des Stacks der Anwendung und wo jedes Objekt hinzeigt und gespeichert wird, wenn wir die Anwendung ausführen.

Wir werden die Ausführung der App durchgehen, jede Zeile anhalten, erklären, wann die Anwendung die Objekte alloziert und sie im Heap oder im Stack speichert. Wir werden auch sehen, wann die Anwendung die Objekte aus dem Stack und dem Heap freigibt.

  • Zeile 1 – Die JVM erstellt einen Stack-Speicherblock für die Hauptmethode.

  • Zeile 2 – In dieser Zeile erstellen wir eine primitive lokale Variable. Die Variable wird erstellt und im Stapelspeicher der Hauptmethode gespeichert.

  • Zeile 3 -Hier brauche ich Ihre Aufmerksamkeit!!! In dieser Zeile erstellen wir ein neues Objekt. Das Objekt wird im Stack der Hauptmethode erstellt und im Heap gespeichert. Der Stack speichert die Referenz, die Objekt-Heap-Speicheradresse (Pointer), während der Heap das Originalobjekt speichert.

  • Zeile 4 – Dasselbe wie Zeile 3.

  • Zeile 5 – Die JVM erstellt einen Stapelspeicherblock für die Methode foo.

  • Zeile 6 – Erzeugt ein neues Objekt. Das Objekt wird im Stapelspeicher der Methode foo erstellt, und wir speichern im Stapel die Heap-Speicheradresse des Objekts, das wir in Zeile 3 erstellt haben. Den Wert (Heap-Speicheradresse des Objekts in Zeile 3) haben wir in Zeile 5 übergeben. Denken Sie daran, dass Java Referenzen immer nach Wert übergibt.

  • Zeile 7 – Wir erstellen ein neues Objekt. Das Objekt wird im Stack erstellt und zeigt auf den String-Pool im Heap.

  • Zeile 8 – In der letzten Zeile der Methode foo wird die Methode beendet. Und die Objekte werden aus dem Stack-Block der foo-Methode freigegeben.

  • Zeile 9- Dasselbe wie in Zeile 8, in der letzten Zeile der main-Methode, die Methode beendet. Und der Stack-Block der Hauptmethode wird frei.

Was ist mit dem Freigeben des Speichers aus dem Heap? Dazu kommen wir gleich noch. Schnapp dir einen Kaffee☕️, und mach weiter 😼.

Was passiert, wenn Methoden beendet werden?

Jede Methode hat ihren eigenen Bereich. Wenn die Methode beendet wird, werden die Objekte freigegeben und automatisch vom Stack zurückgefordert.

Abbildung 1

In Abbildung 1, wenn die fooMethode beendet wird. Der Stapelspeicher oder der Stapelblock der foo-Methode wird automatisch freigegeben und zurückgewonnen.

Abbildung 2

In Abbildung 2 das gleiche. Wenn die Methode main beendet wurde. Der Stapelspeicher oder der Stapelblock der Hauptmethode wird automatisch freigegeben und zurückgewonnen.

Schlussfolgerung

Nun ist uns klar, dass die Objekte im Stapel temporär sind. Sobald die Methode beendet ist, werden die Objekte freigegeben und zurückverlangt.

Der Stapel ist eine LIFO (Last-In-First-Out) Datenstruktur. Man kann ihn als eine Box betrachten. Durch die Verwendung dieser Struktur kann das Programm alle seine Operationen mit Hilfe von zwei einfachen Operationen verwalten: push und pop.

Jedes Mal, wenn Sie etwas wie eine Variable oder eine Methode speichern müssen, wird der Stapelzeiger nach oben geschoben. Jedes Mal, wenn Sie eine Methode verlassen, wird alles vom Stack-Zeiger entfernt, bis Sie zur Adresse der vorherigen Methode zurückkehren. In unserem Beispiel kehren wir von der Methode foo zur Methode main zurück.

Was ist mit dem Heap?

Der Heap unterscheidet sich vom Stack. Um die Objekte aus dem Heap-Speicher freizugeben und wiederzugewinnen, brauchen wir Hilfe.

Dafür hat Java, oder genauer gesagt, die JVM, einen Superhelden geschaffen, der uns hilft. Wir haben ihn den Garbage Collector genannt. Er wird die harte Arbeit für uns erledigen. Er kümmert sich darum, unbenutzte Objekte aufzuspüren, sie freizugeben und mehr Platz im Speicher zu gewinnen.

Wie funktioniert der Garbage Collector?

Einfach. Der Garbage Collector sucht nach ungenutzten oder nicht erreichbaren Objekten. Wenn es ein Objekt im Heap gibt, auf das kein Verweis zeigt, kümmert sich der Garbage Collector darum, es aus dem Speicher zu entfernen und mehr Platz zu gewinnen.

GC

GC-Wurzeln sind Objekte, auf die die JVM verweist. Sie sind die Anfangsobjekte des Baums. Jedes Objekt im Baum hat ein oder mehrere Wurzelobjekte. Solange die Anwendung oder die GC-Roots diese Wurzeln oder Objekte erreichen können, ist der gesamte Baum erreichbar. Sobald sie von der Anwendung oder den GC-Roots nicht mehr erreicht werden können, werden sie als unbenutzte oder unerreichbare Objekte betrachtet.

Was passiert, wenn der Garbage Collector läuft?

Im Moment ist dies der aktuelle Speicherzustand der Anwendung. Der Stack ist leer, und der Heap ist voll von unbenutzten Objekten.

Vor GC

Nach dem Ausführen des GC werden die Ergebnisse wie folgt aussehen:

Nach GC

Die GC wird alle unbenutzten Objekte aus dem Heap freigeben und löschen.

Mann! Was ist mit dem Speicherleck, auf das wir warten? LOL, nur noch ein kleines bisschen mehr, und wir sind da. In unserer einfachen Anwendung war die App großartig und einfach geschrieben. Es gab keinen Fehler im Code, der die GC davon abhalten könnte, die Objekte des Heaps freizugeben. Und aus diesem Grund gibt die GC alle Objekte aus dem Heap-Speicher frei und fordert sie zurück. Machen Sie weiter. Wir haben eine Menge Beispiele für Speicherlecks im nächsten Abschnitt😃.

Wann und wie entstehen Speicherlecks?

Ein Speicherleck entsteht, wenn der Stack noch auf unbenutzte Objekte im Heap verweist.

Zum besseren Verständnis des Konzepts finden Sie in der folgenden Abbildung eine einfache visuelle Darstellung.

In der visuellen Darstellung sehen wir, dass Objekte, die vom Stack referenziert werden, aber nicht mehr in Gebrauch sind, vom Garbage Collector nie freigegeben werden. Der Garbage Collector wird sie niemals aus dem Speicher freigeben, weil er anzeigt, dass diese Objekte in Gebrauch sind, obwohl sie es nicht sind.

Wie können wir ein Leck verursachen?

Es gibt verschiedene Möglichkeiten, ein Speicherleck in Android zu verursachen. Und es kann leicht mit AsyncTasks, Handlern, Singleton, Threads und mehr gemacht werden.

Ich werde einige Beispiele mit Threads, Singleton und Listenern zeigen, um zu erklären, wie wir ein Leck verursachen und sie vermeiden und beheben können.

Sieh dir mein Github-Repository an. Ich habe einige Codebeispiele.

Wie können wir ein Leck mit Hilfe von Threads verursachen?

In diesem Beispiel werden wir eine Aktivität starten, die einen Thread im Hintergrund ausführt. Der Thread wird eine Aufgabe ausführen, die 20 Sekunden dauert.

Wie bekannt, halten die inneren Klassen einen impliziten Verweis auf ihre umschließende Klasse.

Hinter den Kulissen sieht die Aktivität tatsächlich so aus.

Die DownloadTask hält einen Verweis auf die ThreadActivity.

Was passiert nun nach dem Start der Task oder des Threads?

Es gibt zwei mögliche Abläufe für die Nutzung der Anwendung. Ein regulärer Flow, der wie erwartet ohne Fehler funktioniert und ein Leak Flow, der ein Speicherleck verursacht.

Regular Flow

In der Abbildung stellen wir den Heap und den Stack der Anwendung dar.

Der Benutzer startet die Anwendung, öffnet die ThreadActivity und wartete auf dem Bildschirm, bis die Download-Aufgabe beendet war. Der Benutzer hat 20 Sekunden lang gewartet. Warum 20-Sekunden? Weil dies die Zeit ist, die der Thread benötigt, um die Aufgabe abzuschließen.

Aufgabe läuft

Die Aufgabe wird im Hintergrund ausgeführt. Der Benutzer hat 20 Sekunden lang auf den Abschluss der Download-Aufgabe gewartet. Wenn die Aufgabe abgeschlossen ist, gibt der Stack den Methodenblock run() frei.

Es gibt keine Referenz, die die DownloadTask hält. Der GC betrachtet das DownladTask-Objekt als ein unbenutztes Objekt, und aus diesem Grund wird es im nächsten GC-Zyklus aus dem Heap-Speicher gelöscht.

Der GC löscht die unbenutzten Objekte aus dem Heap. Wenn der Benutzer nun die Aktivität schließt. Die Hauptmethode wird vom Stack freigegeben, und im nächsten Zyklus der GC wird die GC die ThreadActivity aus dem Heap-Speicher löschen.

Perfekt!