Java Garbage Collection
Wie Java Garbage Collection funktioniert: Erreichbarkeit, GC-Roots, generationaler Heap, Mark-Sweep-Compact, Referenztypen und Collector-Auswahl.
In Java rufen Sie niemals free() auf. Die JVM verfolgt jedes Objekt, das Sie auf dem Heap allozieren, und wenn ein Objekt von Ihrem laufenden Programm nicht mehr erreicht werden kann, gibt der Garbage Collector (GC) seinen Speicher automatisch frei. Sie schreiben Code, der Objekte erzeugt; der GC räumt still hinter Ihnen auf. Zu verstehen, wie er entscheidet, was Garbage ist — und wo im Heap er sucht — ist der Unterschied zwischen Code, der skaliert, und Code, der unter Last ins Stocken gerät.
Diese Seite behandelt, wie der GC entscheidet, was er behält (Erreichbarkeit), wie der Heap für schnelle Collection aufgeteilt ist, den Mark-Sweep-Compact-Algorithmus, die vier Referenzstärken, die Wahl eines Collectors und eine ausführbare Demo, die Collection beobachtbar macht.
Erreichbarkeit und GC-Roots
Der GC sucht nicht nach Objekten, mit denen Sie „fertig" sind. Er sucht nach Objekten, die noch erreichbar sind. Ausgehend von einer Menge von GC-Roots folgt er jeder Referenz. Alles, was er erreichen kann, ist lebendig; alles andere ist Garbage, unabhängig davon, ob Sie glauben, es noch zu benötigen.
| GC-Root | Beispiel |
|---|---|
| Lokale Variablen | Eine Referenz auf dem Stack eines laufenden Threads |
| Statische Felder | static final Logger LOG = ... |
| Aktive Threads | Ein lebendiges Thread-Objekt |
| JNI-Referenzen | Objekte, die von nativem Code gehalten werden |
Eine Referenz auf null zu setzen (oder sie aus dem Gültigkeitsbereich fallen zu lassen) löscht nichts — es entfernt nur einen Pfad zum Objekt. Das Objekt wird nur dann sammelbar, wenn kein Pfad von einer Root mehr übrig ist.
Object a = new Object(); // reachable via local variable 'a'
Object b = a; // now two references point to the same object
a = null; // still reachable through 'b' — not garbage
b = null; // now unreachable — eligible for collectionDer generationale Heap
Die meisten Objekte sterben jung — ein Request-Scope, eine Schleifenvariable, ein Zwischenstring. Die JVM nutzt diese schwache generationale Hypothese, indem sie den Heap in Regionen aufteilt und den jungen Bereich viel häufiger sammelt als den alten.
| Region | Enthält | Gesammelt |
|---|---|---|
| Young (Eden + 2 Survivor-Spaces) | Neu allozierte Objekte | Häufig, durch einen schnellen Minor GC |
| Old (Tenured) | Objekte, die mehrere Minor GCs überlebt haben | Selten, durch einen langsameren Major/Full GC |
| Metaspace | Klassen-Metadaten (nicht Ihre Objekte) | Wenn Classloader entladen werden |
Neue Objekte landen in Eden. Ein Minor GC kopiert die wenigen Überlebenden in einen Survivor-Space; Objekte, die weiter überleben, werden schließlich in die alte Generation befördert. Da Minor GCs nur die kleine junge Region scannen, sind sie günstig — weshalb kurzlebige Allokation in Java schnell ist. (Wie Stack und Heap sich unterscheiden, erfahren Sie unter Stack vs Heap; zur Rolle der JVM, die den Heap hostet, siehe JVM Architecture.)
Mark, Sweep, Compact
Eine Collection läuft in Phasen ab. Zuerst markiert sie jedes erreichbare Objekt, indem sie den Graphen von den Roots aus durchläuft. Dann sweept sie und gibt die nicht markierten Objekte frei. Viele Collectors fügen eine Compact-Phase hinzu, die überlebende Objekte zusammenschiebt, sodass freier Speicher ein zusammenhängender Block ist — was die Allokation zu einem einfachen Pointer-Bump macht und Fragmentierung verhindert.
// Pseudocode of what the collector does for you:
// 1. mark: visit(roots); for each reachable object, set live = true
// 2. sweep: for each object on the heap, if !live -> reclaim its memory
// 3. compact: move survivors next to each other, update referencesSie können eine Collection mit System.gc() vorschlagen, aber das ist nur ein Hinweis — die JVM kann ihn ignorieren. Verlassen Sie sich niemals auf Korrektheit; behandeln Sie ihn als Diagnosewerkzeug, nicht als Speicherverwaltungsstrategie.
Referenzstärke
Nicht jede Referenz hält ein Objekt gleich stark am Leben. Das Paket java.lang.ref erlaubt es Ihnen, dem GC mitzuteilen, wie dringend Sie ein Objekt behalten möchten, was die Grundlage speichersensitiver Caches ist.
| Referenz | GC-Verhalten |
|---|---|
Stark (gewöhnliches =) | Wird niemals gesammelt, solange erreichbar |
SoftReference | Wird nur gesammelt, wenn Speicher knapp ist — gut für Caches |
WeakReference | Wird beim nächsten GC gesammelt, sobald keine starken Refs mehr vorhanden sind |
PhantomReference | Wird verwendet, um Aufräumarbeiten nach der Collection zu planen |
import java.lang.ref.WeakReference;
byte[] data = new byte[1024];
WeakReference<byte[]> ref = new WeakReference<>(data);
data = null; // drop the only strong reference
// After the next GC, ref.get() may return null.Speicherlecks können trotzdem auftreten
Ein Garbage Collector befreit Sie von hängenden Zeigern und doppeltem Freigeben, aber nicht von Lecks. Ein Java-Speicherleck ist ein Objekt, das Sie nicht mehr verwenden, das aber noch von einer Root aus erreichbar ist, sodass der GC es behalten muss. Der Heap füllt sich, der GC läuft immer häufiger, und schließlich erhalten Sie einen OutOfMemoryError.
Die klassischen Ursachen sind alle „Ich habe vergessen loszulassen":
- Eine
staticCollection (Cache, Listener-Liste, Map), der Sie immer wieder hinzufügen, aber nie etwas entnehmen. Statische Felder sind GC-Roots, also lebt alles, was sie erreichen, für immer. - Listener oder Callbacks, die bei einem langlebigen Objekt registriert und nie abgemeldet wurden.
- Schlüssel, die lange nach ihrer Notwendigkeit in einer
HashMapverbleiben, weil die Map sie noch referenziert.
Die Lösung ist kein Flag — es geht darum, Referenzen loszulassen, wenn man fertig ist (aus der Collection entfernen, den Listener abmelden) oder eine WeakReference-basierte Struktur wie WeakHashMap zu verwenden, damit der GC Einträge zurückfordern kann, sobald nichts anderes mehr auf den Schlüssel zeigt.
finalize() ist veraltet und unzuverlässig — die JVM kann es spät oder gar nicht ausführen. Um Dateien, Sockets oder andere Nicht-Speicher-Ressourcen deterministisch freizugeben, verwenden Sie try-with-resources und AutoCloseable, nicht den Garbage Collector.Einen Collector wählen
Die HotSpot JVM wird mit mehreren Collectors ausgeliefert, die unterschiedliche Abwägungen zwischen Durchsatz (geleistete Gesamtarbeit) und Latenz (Länge der Pausen) bieten. Sie wählen einen mit einem JVM-Flag; der Standard seit Java 9 ist G1.
| Collector | Flag | Am besten für |
|---|---|---|
| G1 (Standard) | -XX:+UseG1GC | Ausgeglichene Latenz/Durchsatz, große Heaps |
| Parallel | -XX:+UseParallelGC | Batch-Jobs, die reinen Durchsatz bevorzugen |
| ZGC | -XX:+UseZGC | Sehr große Heaps, Pausen unter einer Millisekunde |
| Serial | -XX:+UseSerialGC | Kleine Heaps, Einzel-Core oder Container |
# Pick a collector and set the heap size at launch:
java -XX:+UseG1GC -Xms256m -Xmx2g MyApp
# Print what the GC is doing, with timestamps:
java -Xlog:gc* MyAppEin ausgearbeitetes Beispiel
Das folgende Programm macht das Verhalten des GC beobachtbar. Es fixiert ein Objekt mit einer starken Referenz, hält ein anderes nur durch eine WeakReference, erzeugt eine Welle kurzlebigen Garbage, fragt dann nach einer Collection und berichtet, was überlebt hat und wie sich die Heap-Nutzung verändert hat.
Was aus dem Lauf mitgenommen werden sollte:
- Das schwach referenzierte Objekt gibt vor der Collection
trueund danachfalseaus, was beweist, dass eineWeakReferenceihr Objekt nicht am Leben hält, sobald keine starke Referenz mehr vorhanden ist. - Das stark gehaltene
kept-Array gibtsurvived: trueaus, selbst nachSystem.gc(), weil es von einer GC-Root aus erreichbar ist und der Collector es bewahren muss. - Ungefähr 300 MB Garbage wird alloziert (
Bytes allocated as garbage: 307200000), dennoch steigt der verwendete Heap nur auf etwa 5 MB — Minor GCs fordern die kurzlebigen Schleifen-Arrays so schnell zurück, wie sie erzeugt werden. Runtime.maxMemory()meldet die Heap-Obergrenze (hier etwa 256 MB), festgelegt durch-Xmx, währendtotalMemory() - freeMemory()der live genutzte Anteil ist, der bei 3–5 MB bleibt.System.gc()ist nur ein Hinweis, aber auf dieser JVM wird er ausgeführt: der verwendete Heap fällt wieder ab und das unerreichbare schwach referenzierte Objekt wird geleert statt zu verbleiben.