Java GC-Algorithmen
Vergleich der wichtigsten Java-Garbage-Collector — Serial, Parallel, G1, ZGC und Shenandoah — mit ihren jeweiligen Kompromissen.
Die JVM befreit Sie davon, Speicher manuell freizugeben: Ein Garbage Collector (GC) läuft im Hintergrund, findet Objekte, die Ihr Programm nicht mehr erreichen kann, und gibt deren Speicherplatz frei. „Der Garbage Collector" ist jedoch kein einheitliches Konzept. Die HotSpot-JVM liefert mehrere Collector mit, die jeweils einen anderen Kompromiss zwischen Durchsatz (wie viel CPU Ihrer Anwendung statt dem GC zugutekommt), Latenz (wie lange die Anwendung pausiert, während der GC arbeitet) und Footprint (wie viel Overhead der Collector selbst verursacht) eingehen.
Die richtige Wahl ist wichtig. Ein Batch-Job, der über Nacht Zahlen verarbeitet, möchte maximalen Durchsatz und kümmert sich nicht um Pausen; ein Handelssystem oder ein Webserver möchte die kürzestmöglichen Pausen, selbst wenn der Gesamtdurchsatz etwas sinkt. Dieses Kapitel vergleicht die produktiven Collector und zeigt, was sie alle gemeinsam haben: Ein Objekt lebt genau so lange, wie es erreichbar ist.
Wie Collector entscheiden, was behalten wird
Jeder HotSpot-Collector beantwortet dieselbe Frage — welche Objekte noch in Verwendung sind — auf dieselbe Weise: Er verfolgt die Erreichbarkeit von einer Menge von GC-Wurzeln (lokale Variablen auf dem Stack, statische Felder, aktive Threads). Jedes von einer Wurzel erreichbare Objekt ist lebendig; alles andere ist Garbage. Gültigkeitsbereich und Alter sind irrelevant; nur die Erreichbarkeit zählt. Einen breiteren Überblick, wie dies zur Laufzeit passt, finden Sie unter Java Garbage Collection und Stack vs Heap.
public class Reachability {
public static void main(String[] args) {
String a = new String("kept"); // reachable via local variable 'a'
String b = new String("dropped"); // reachable via 'b'...
b = null; // ...until now: "dropped" is unreachable
System.out.println(a); // 'a' is still a GC root reference
}
}In dem Moment, in dem b = null ausgeführt wird, hat das Objekt "dropped" keinen Pfad von einer Wurzel mehr und ist für die Collection freigegeben. Der Collector kann es sofort, viel später oder — wenn das Programm zuerst beendet wird — überhaupt nicht freigeben. Sie rufen nie free auf; Sie hören einfach auf zu referenzieren.
Generationale Heap-Struktur
Die meisten Java-Objekte sterben jung. Collector nutzen dies mit einem generationalen Heap: Neue Objekte landen in der jungen Generation (Eden plus zwei Survivor-Bereiche), und Objekte, die mehrere Collections überleben, werden in die alte Generation befördert. Die kleine, stark mit Garbage gefüllte junge Generation häufig zu sammeln ist günstig; die große alte Generation wird viel seltener gesammelt.
| Bereich | Was hier lebt | Wie oft gesammelt |
|---|---|---|
| Eden | Frisch allokierte Objekte | Bei jeder Minor GC |
| Survivor (S0/S1) | Objekte, die eine Minor GC überlebt haben | Bei jeder Minor GC |
| Old (tenured) | Langlebige, beförderte Objekte | Major / Full GC |
| Metaspace | Klassen-Metadaten (off-heap) | Beim Entladen von Klassen |
Eine Minor GC bereinigt die junge Generation und ist schnell; eine Major oder Full GC berührt die alte Generation und ist die Quelle der langen Pausen, vor denen man sich fürchtet.
Die Collector im Vergleich
HotSpot lässt Sie einen Collector mit einem einzigen Flag auswählen, und jeder ist auf ein anderes Ziel ausgerichtet. Sie ändern den Algorithmus selten im Code — Sie legen ihn in der Befehlszeile fest.
java -XX:+UseSerialGC MyApp # single-threaded, tiny heaps
java -XX:+UseParallelGC MyApp # throughput-oriented, multi-threaded
java -XX:+UseG1GC MyApp # balanced, the default since Java 9
java -XX:+UseZGC MyApp # sub-millisecond pauses, huge heaps
java -XX:+UseShenandoahGC MyApp # low pause, concurrent compactionDie folgende Tabelle ist das Denkmodell, das man im Kopf behalten sollte:
| Collector | Stärke | Pausenverhalten | Typischer Einsatz |
|---|---|---|---|
| Serial | Einfachste, geringer Footprint | Stop-the-world, ein Thread | Kleine Heaps, Container, CLIs |
| Parallel | Höchster Durchsatz | Stop-the-world, viele Threads | Batch / Datenverarbeitung |
| G1 | Ausgewogen, vorhersehbar | Überwiegend concurrent, Pausenziel | Allgemeiner Standard |
| ZGC | Sehr niedrige Latenz | Sub-Millisekunden, concurrent | Multi-GB bis TB-Heaps |
| Shenandoah | Sehr niedrige Latenz | Pausen unabhängig von der Heap-Größe | Reaktionsfähige Dienste |
G1 („Garbage-First") ist ab Java 9 der Standard. Er teilt den Heap in gleich große Regionen auf und sammelt zuerst die Regionen mit dem meisten Garbage, wobei er auf ein von Ihnen mit -XX:MaxGCPauseMillis=200 gesetztes Pausenziel hinarbeitet.
Concurrent vs. Stop-the-world
Die entscheidende Dimension ist, wann die Anwendungsthreads anhalten müssen. Stop-the-world (STW)-Collector (Serial, Parallel) pausieren alle Anwendungsthreads, während sie arbeiten — einfach und leistungsstark, aber die Pause wächst mit dem Heap. Concurrent-Collector (ZGC, Shenandoah und ein Großteil von G1) erledigen den Großteil ihrer Arbeit, während Ihre Threads weiterlaufen, sodass Pausen auch bei Heaps von Gigabytes oder Terabytes kurz bleiben.
# See exactly what the collector is doing and how long it pauses
java -Xlog:gc -XX:+UseG1GC MyApp
# Sample output line:
# [0.412s][info][gc] GC(3) Pause Young (Normal) (G1 Evacuation Pause) 24M->5M(64M) 1.832msDiese Log-Zeile ist es wert, entschlüsselt zu werden: 24M->5M(64M) bedeutet, dass der genutzte Heap von 24 MB auf 5 MB von insgesamt 64 MB gesunken ist, und die Anwendung für 1.832ms pausierte. Das Lesen von -Xlog:gc-Ausgaben ist die nützlichste einzelne GC-Tuning-Fähigkeit — messen Sie, bevor Sie ein Flag ändern.
Collection aus dem Code heraus beobachten
Sie können von Java aus keinen spezifischen Algorithmus direkt aufrufen, aber Sie können die Collection beobachten. Eine WeakReference ermöglicht es Ihnen, einen Zeiger zu halten, der sein Ziel nicht am Leben hält, sodass Sie fragen können: „Wurde dieses Objekt bereits gesammelt?" Die Runtime-Klasse meldet die Heap-Nutzung, und System.gc() ist ein Hinweis — niemals ein Befehl — eine Collection jetzt durchzuführen.
import java.lang.ref.WeakReference;
WeakReference<byte[]> ref = new WeakReference<>(new byte[1024]);
System.out.println("Before GC: " + (ref.get() != null)); // true
System.gc();
System.out.println("After GC: " + (ref.get() != null)); // usually falseDas ausführbare Beispiel unten verbindet diese Teile: Es allokiert eine Welle von Garbage, hält einen Überlebenden erreichbar, beobachtet ein unerreichbares Objekt durch eine schwache Referenz und misst den Heap vor und nach einer Collection.
Was man aus dem Durchlauf mitnehmen kann:
- Schritt 2 gibt
trueaus: Das beobachtete Objekt ist noch stark erreichbar über die Variablegarbage, sodass dieWeakReferencees lesen kann. - Schritt 3 meldet den genutzten Heap nach 300.000 kurzlebigen Allokationen. Die genaue Zahl variiert von Durchlauf zu Durchlauf — eine Minor GC könnte diese Welle bereits mitten in der Schleife großteils bereinigt haben — aber ihre Erstellung ist genau der Young-Generation-Umsatz, für den jeder generationale Collector kostengünstig ausgelegt ist.
- Schritt 4 gibt
trueaus und bestätigt, dass das beobachtete Objekt freigegeben wurde, sobaldgarbage = nulles unerreichbar machte undSystem.gc()eine Collection auslöste — Beweis dafür, dass der Verlust der Erreichbarkeit, nicht das Verlassen des Gültigkeitsbereichs, Speicher freigibt. - Schritt 5 gibt
trueaus: Dersurvivor, der noch von einer lebendigen lokalen Variable (einer GC-Wurzel) referenziert wird, übersteht die Collection unberührt. - Schritt 6 zeigt, wie der genutzte Heap nahe dem Ausgangswert zurückfällt, was zeigt, dass der Collector den freigegebenen Speicher zur Wiederverwendung zurückgibt, anstatt dass das Programm ihn verliert.
Mehr zu den hier verwendeten Referenztypen — stark, soft, schwach und phantom — finden Sie unter Java References.