Die Java-Speicherverwaltung mit ihrer eingebauten Garbage Collection ist eine der besten Errungenschaften der Sprache. Sie ermöglicht es Entwicklern, neue Objekte zu erstellen, ohne sich explizit um die Zuweisung und Freigabe von Speicher kümmern zu müssen, da der Garbage Collector den Speicher automatisch zur Wiederverwendung zurückfordert. Dies ermöglicht eine schnellere Entwicklung mit weniger Standardcode und eliminiert gleichzeitig Speicherlecks und andere speicherbezogene Probleme. Zumindest in der Theorie.
Eigentlich scheint die Garbage Collection in Java zu gut zu funktionieren, da sie zu viele Objekte erstellt und entfernt. Die meisten Probleme mit der Speicherverwaltung sind gelöst, aber oft um den Preis, dass sie zu ernsthaften Leistungsproblemen führen. Die Anpassung der Garbage Collection an alle möglichen Situationen hat zu einem komplexen und schwer zu optimierenden System geführt. Um die Garbage Collection zu verstehen, müssen Sie zunächst wissen, wie die Speicherverwaltung in einer Java Virtual Machine (JVM) funktioniert.
Wie die Java Garbage Collection wirklich funktioniert
Viele Leute denken, dass die Garbage Collection tote Objekte sammelt und verwirft. In Wirklichkeit macht die Java Garbage Collection das Gegenteil! Lebende Objekte werden verfolgt und alles andere als Müll bezeichnet. Wie Sie sehen werden, kann dieses grundlegende Missverständnis zu vielen Leistungsproblemen führen.
Beginnen wir mit dem Heap, dem Speicherbereich, der für dynamische Zuweisungen verwendet wird. In den meisten Konfigurationen weist das Betriebssystem den Heap im Voraus zu, damit er von der JVM verwaltet werden kann, während das Programm läuft. Dies hat einige wichtige Auswirkungen:
- Die Erstellung von Objekten ist schneller, da eine globale Synchronisierung mit dem Betriebssystem nicht für jedes einzelne Objekt erforderlich ist. Eine Zuweisung beansprucht einfach einen Teil eines Speicherfeldes und verschiebt den Offset-Zeiger nach vorne (siehe Abbildung 2.1). Die nächste Zuweisung beginnt an diesem Offset und beansprucht den nächsten Teil des Arrays.
- Wenn ein Objekt nicht mehr verwendet wird, fordert der Garbage Collector den zugrunde liegenden Speicher zurück und verwendet ihn für zukünftige Objektzuweisungen wieder. Das bedeutet, dass es kein explizites Löschen gibt und kein Speicher an das Betriebssystem zurückgegeben wird.
Abbildung 2.1: Neue Objekte werden einfach am Ende des benutzten Heaps zugewiesen.
Alle Objekte werden auf dem von der JVM verwalteten Heap-Bereich zugewiesen. Jedes Objekt, das der Entwickler verwendet, wird auf diese Weise behandelt, einschließlich Klassenobjekte, statische Variablen und sogar der Code selbst. Solange ein Objekt referenziert wird, betrachtet die JVM es als lebendig. Sobald ein Objekt nicht mehr referenziert wird und somit für den Anwendungscode nicht mehr erreichbar ist, wird es vom Garbage Collector entfernt und der ungenutzte Speicher zurückverlangt. So einfach das klingt, so sehr stellt sich die Frage: Was ist der erste Verweis im Baum?
Garbage-Collection Roots-Die Quelle aller Objektbäume
Jeder Objektbaum muss ein oder mehrere Wurzelobjekte haben. Solange die Anwendung diese Wurzeln erreichen kann, ist der gesamte Baum erreichbar. Aber wann werden diese Wurzelobjekte als erreichbar angesehen? Spezielle Objekte, die als Garbage-Collection-Roots (GC-Roots; siehe Abbildung 2.2) bezeichnet werden, sind immer erreichbar, ebenso wie jedes Objekt, das eine Garbage-Collection-Root an seiner eigenen Wurzel hat.
Es gibt vier Arten von GC-Roots in Java:
- Lokale Variablen werden durch den Stack eines Threads am Leben gehalten. Es handelt sich dabei nicht um eine virtuelle Referenz auf ein reales Objekt und ist daher nicht sichtbar. In jeder Hinsicht sind lokale Variablen GC-Roots.
- Aktive Java-Threads werden immer als lebende Objekte betrachtet und sind daher GC-Roots. Dies ist besonders wichtig für lokale Thread-Variablen.
- Statische Variablen werden von ihren Klassen referenziert. Diese Tatsache macht sie de facto zu GC-Roots. Klassen selbst können garbage-collected werden, was alle referenzierten statischen Variablen entfernen würde. Dies ist von besonderer Bedeutung, wenn wir Anwendungsserver, OSGi-Container oder Klassenlader im Allgemeinen verwenden. Wir werden die damit verbundenen Probleme im Abschnitt Problemmuster erörtern.
- JNI-Referenzen sind Java-Objekte, die der native Code als Teil eines JNI-Aufrufs erstellt hat. Die so erzeugten Objekte werden besonders behandelt, da die JVM nicht weiß, ob sie vom nativen Code referenziert werden oder nicht. Solche Objekte stellen eine ganz besondere Form von GC-Roots dar, die wir im Abschnitt Problem Patterns genauer untersuchen werden.
Abbildung 2.2: GC-Roots sind Objekte, die selbst von der JVM referenziert werden und so verhindern, dass jedes andere Objekt garbage-collected wird.
Eine einfache Java-Anwendung hat also folgende GC-Roots:
- Lokale Variablen in der Main-Methode
- Der Main-Thread
- Statische Variablen der Main-Klasse
Markieren und Wegfegen von Garbage
Um festzustellen, welche Objekte nicht mehr verwendet werden, führt die JVM in regelmäßigen Abständen einen Algorithmus aus, der sehr treffend Mark-and-Sweep genannt wird. Es handelt sich dabei um einen einfachen, zweistufigen Prozess:
- Der Algorithmus durchläuft alle Objektreferenzen, beginnend mit den GC-Roots, und markiert jedes gefundene Objekt als lebendig.
- Der gesamte Heap-Speicher, der nicht von markierten Objekten belegt ist, wird zurückgewonnen. Er wird einfach als frei markiert und im Wesentlichen von ungenutzten Objekten befreit.
Die Müllabfuhr soll die Ursache für klassische Speicherlecks beseitigen: nicht erreichbare, aber nicht gelöschte Objekte im Speicher. Dies funktioniert jedoch nur bei Speicherlecks im ursprünglichen Sinne. Es ist möglich, dass ungenutzte Objekte noch von einer Anwendung erreicht werden können, weil der Entwickler einfach vergessen hat, sie zu derefenzieren. Solche Objekte können nicht in den Müll geworfen werden. Noch schlimmer ist, dass ein solches logisches Speicherleck von keiner Software erkannt werden kann (siehe Abbildung 2.3). Selbst die beste Analysesoftware kann nur verdächtige Objekte hervorheben. Wir werden die Analyse von Speicherlecks im Abschnitt Analyse der Leistungsauswirkungen von Speichernutzung und Garbage Collection weiter unten untersuchen.