← Zurück

Kapitelkompass

Vom Symptom zur JVM-Hypothese

Heap, GC, CPU, Threads und Container müssen als korrelierter Snapshot gelesen werden; eine Einzelmetrik beweist keine Ursache.

SymptomAusgangspunktSnapshotEinordnenHypotheseAbsichernVergleichErgebnis
Der Themenweg zeigt die fachliche Leserichtung dieses Kapitels.

Das nimmst du mit

  • Heap und RSS trennen
  • GC-Signale lesen
  • CPU und Locks unterscheiden
  • Vorher/Nachher messen

Praxisfall

p95 steigt, Heap liegt bei 88 Prozent und die Allokationsrate erreicht 240 MiB/s.

Entscheidung

Erst Evidenz mit JFR, GC-Log, Thread Dump oder NMT sichern; danach genau eine Variable ändern.

!

Typisches Risiko

Mehr Heap oder ein anderer Collector kann Symptome verschieben und Container-Restarts verschärfen.

Schnelle Einordnung
SignalLesartNächster Schritt
Heap hoch, RSS passendJava-Heap oder RetentionJFR Allocation und Heap nach GC untersuchen
Heap moderat, RSS hochNative Memory oder Thread-StacksNMT, Direct Buffers und Threadzahl prüfen
CPU hoch, wenig BlockingHot Path oder GC-ArbeitExecution Profile und GC-Zeit korrelieren

15. JVM, Speicher, GC und Profiling

JVM, Speicher, GC, Classloading und Profiling

1. Orientierung: JVM Deep Dive statt JVM-Mythen

In Enterprise-Projekten wird die JVM oft erst beachtet, wenn ein Incident läuft: CPU 100 %, Latenz steigt, Pods werden wegen OOMKilled beendet oder ein Batch wird jede Nacht langsamer. Ein Deep Dive beginnt nicht mit Tuning-Flags, sondern mit einem Mentalmodell: Java-Code wird zu Bytecode, die JVM lädt Klassen, führt Methoden aus, optimiert Hot Paths, verwaltet Speicher, plant Threads und räumt nicht mehr erreichbare Objekte auf.

Typischer Fehler: Man sucht sofort nach dem richtigen GC-Flag. Besser ist zuerst die Diagnose: Ist das Problem CPU-bound, Allocation-bound, IO-bound, lock-bound, classloading-bezogen oder ein falsches Container-Limit? Erst dann lohnt Tuning.

2. Java-Code, Bytecode, Interpreter und JIT

Der Compiler javac übersetzt Java-Quelltext in Bytecode. Dieser Bytecode ist nicht Maschinencode, sondern eine plattformunabhängige Zwischenform. Die JVM interpretiert zunächst, erkennt häufig ausgeführte Hot Paths und kann diese durch den JIT-Compiler optimieren.

Schlechte Schlussfolgerung: “Java ist langsam, weil interpretiert.” In Wirklichkeit ist moderne JVM-Ausführung eine Kombination aus Interpretation, Profiling und Just-in-Time-Kompilierung.

JAVA
// Ein kleiner Hot Path: viele Aufrufe erlauben der JVM Optimierungen.
long sum = 0;
for (int i = 0; i < 10_000_000; i++) {
    sum += priceInCent(i);
}

Produktionshinweis: Microbenchmarks ohne Warmup sind meistens falsch. Ein einmaliger Methodenaufruf misst oft Classloading, JIT-Warmup, Allocation und Betriebssystemeffekte mit.

3. Heap, Stack und Metaspace

Der Heap enthält Objekte und Arrays. Der Stack enthält pro Thread Frames und lokale Referenzen. Der Metaspace enthält Klassenmetadaten. Diese Bereiche haben unterschiedliche Fehlerbilder.

Wenn der Heap voll wirkt, ist die erste Frage nicht “welcher GC?”, sondern: Welche Objekte bleiben nach einer Full-GC-ähnlichen Bereinigung erreichbar? Wenn der Stack betroffen ist, sucht man eher nach Rekursion oder zu vielen verschachtelten Aufrufen. Wenn Metaspace wächst, denkt man an Classloader, dynamische Proxies, Plugins, Deployments oder Leaks in Container-Umgebungen.

4. Objektlebensdauer und Allocation Pressure

Viele kurzlebige Objekte sind für die JVM normal. Problematisch wird es, wenn pro Request unnötig große Objektgraphen entstehen oder wenn Objekte länger leben als gedacht.

Schlechte Lösung:

JAVA
// jedes Mapping erzeugt unnötig große Zwischenstrukturen
List<OrderView> views = orders.stream()
.map(order -> new OrderView(order, loadAllCustomerData(order.customerId())))
.toList();

Besser ist eine bewusste Sicht: benötigte Felder laden, Batch-Zugriff nutzen, Objektgraph klein halten. Deep-Dive-Frage: Welche Objekte entstehen pro Request, wie groß sind sie, wie lange bleiben sie erreichbar, und wo werden sie referenziert?

5. Garbage Collection: was GC kann und was nicht

Garbage Collection sammelt nicht mehr erreichbare Objekte. Sie repariert keine schlechten Referenzpfade. Ein Memory Leak in Java bedeutet oft: Ein Objekt ist fachlich tot, aber technisch noch erreichbar.

Wichtige Beobachtung: Wenn die Heap-Baseline nach GC langsam steigt, ist das verdächtig. Wenn der Heap zwischen GCs stark schwankt und danach wieder fällt, ist das oft normale Allocation Pressure. Deshalb ist eine einzelne Heap-Kurve ohne GC-Ereignisse schwer interpretierbar.

6. Memory Leak: Referenzpfade verstehen

Ein Leak entsteht häufig durch statische Collections, Caches ohne Begrenzung, ThreadLocals, Listener, Scheduler, Classloader oder falsch gehaltene Session-Daten.

JAVA
// Pattern: Anti-Pattern - unbounded static registry.
private static final List<byte[]> retained = new ArrayList<>();

Besser: klare Owner, begrenzte Caches, Lifecycle-Methoden, Weak References nur dort, wo sie fachlich passen, und Messung über Heap Dumps oder JFR.

7. Caches: hilfreich, gefährlich, erklärungspflichtig

Ein Cache ist ein bewusstes Produktivitäts- und Performance-Werkzeug. Er ist aber auch ein Speichervertrag. Jeder Cache braucht Antwort auf diese Fragen: Maximalgröße? TTL? Invalidation? Metriken? Warmup? Was passiert bei falschen Daten?

JAVA
// Pattern: Bounded Cache - begrenzt retained objects.
new RetainedCache<String, byte[]>(100);

Ein unbounded Cache ist im Enterprise-Kontext selten akzeptabel. Er kann Tests bestehen und trotzdem nach Wochen Produktion ausfallen.

8. Classloading und Metaspace

Classloading ist im Alltag unsichtbar, bis es schiefgeht. In Servern, Plugin-Systemen, Application Servern und Testumgebungen können Classloader über Deployments hinweg Klassen und statische Referenzen halten.

Typischer Fehler: Ein statischer Singleton hält eine Klasse aus einem alten Deployment. Dadurch bleibt der Classloader erreichbar, und damit bleiben Klassenmetadaten und Objekte erhalten. Das ist besonders in Legacy-Enterprise-Umgebungen relevant.

9. Threading, Stack und Blocking

Jeder Plattform-Thread bringt eigenen Stack und Scheduling-Kosten mit. Viele blockierende Threads können Speicher und Scheduler belasten. Virtual Threads verändern die Kostenstruktur, lösen aber nicht automatisch Datenbank-Connection-Pool-Grenzen, Locking-Probleme oder externe Rate Limits.

Deep-Dive-Frage: Warten Threads auf IO, auf Locks, auf CPU oder auf Ressourcen wie DB-Verbindungen? Ohne diese Unterscheidung ist jedes Tuning geraten.

10. Profiling ohne Selbstbetrug

Profiling ist ein Kreislauf: Symptom beobachten, messen, Hypothese formulieren, eine Änderung machen, erneut messen.

Schlechter Prozess: “Wir erhöhen Heap und hoffen.” Besser: Heap nach GC prüfen, Allocation Rate messen, Hot Methods identifizieren, Thread States betrachten und nur eine Änderung auf einmal testen.

11. JMX, MXBeans und einfache Runtime-Messung

Schon mit Standard-JDK-Mitteln kann man viel sehen: MemoryMXBean, GarbageCollectorMXBean, ClassLoadingMXBean und ThreadMXBean. Diese APIs ersetzen kein professionelles Profiling, helfen aber beim Aufbau eines mentalen Modells.

JAVA
MemoryMXBean memory = ManagementFactory.getMemoryMXBean();
long used = memory.getHeapMemoryUsage().getUsed();

Im Code-Lab wird genau das genutzt: ohne externe Library, damit die Konzepte sichtbar bleiben.

12. GC Logs und Ereignisse lesen

GC Logs sind keine Magie. Man betrachtet Zeitpunkte, Pause, vor/nach Speicher, Ursache und Häufigkeit. Entscheidend ist nicht nur eine lange Pause, sondern der Zusammenhang mit Last, Allocation Rate, Old-Generation-Wachstum und Container-Limits.

Naive Interpretation: “GC läuft oft, also ist GC schlecht.” Besser: Warum läuft er oft? Sind viele kurzlebige Objekte normal? Bleibt nach GC zu viel erhalten? Ist Heap zu klein oder gibt es Retention?

13. Container, Kubernetes und JVM-Limits

In Containern zählt nicht nur -Xmx, sondern das Zusammenspiel aus Container Memory Limit, Heap, Metaspace, Code Cache, Thread Stacks, Direct Buffers und nativen Strukturen. Ein Pod kann OOMKilled werden, obwohl der Java-Heap nicht am Maximum ist.

Produktionshinweis: Heap sollte nicht blind auf das Container-Limit gesetzt werden. Die JVM braucht außerhalb des Heaps ebenfalls Speicher.

14. Typische Produktionsvorfälle

Beispiele: Ein Report lädt 500.000 Entities vollständig in den Heap. Ein Cache wächst ohne Begrenzung. Ein ThreadLocal hält Request-Daten. Ein Scheduler erzeugt parallele Jobs. Ein Classloader-Leak tritt nach Redeploy auf. Eine neue JSON-Library allokiert massiv Zwischenobjekte.

Die gemeinsame Frage lautet: Welche Ressource wird knapp, warum wird sie knapp, und welche fachliche Änderung hat das ausgelöst?

15. Code-Walkthrough: das Kapitel-Lab

Das Code-Lab zeigt absichtlich keine Framework-Magie. Es nutzt JDK-21-Mittel: MXBeans, bounded Cache, Leak-Simulation, WeakReference, GC-Snapshot, einfachen Timer, Thread-State-Sampler und GC-Log-Parser.

Wichtig: Die Beispiele sind klein genug zum Kompilieren, aber fachlich an Produktionsproblemen orientiert. Sie zeigen nicht “so tunen wir GC”, sondern “so denken wir über Speicher, Referenzen und Messung”.

16. Tests und Messgrenzen

Performance-Tests sind empfindlich. Die Tests im Lab prüfen deshalb keine exakten Zeiten, sondern Eigenschaften: begrenzter Cache entfernt ältere Einträge, Registry kann geleert werden, Parser erkennt GC-Zeilen, Timer zeichnet Samples auf, Classloading-Probe liefert sinnvolle Daten.

Exakte Latenztests gehören in kontrollierte Benchmarks. Für Architektur-Lernzwecke sind Eigenschaftstests robuster.

17. Checkliste für JVM-Incidents

  1. Was ist das Symptom: CPU, Speicher, Latenz, OOMKill, Thread-Stau? 2. Gibt es eine Änderung: Release, Last, Datenvolumen, Konfiguration? 3. Was sagen Heap nach GC, Thread States, GC-Pausen, Container-Metriken? 4. Welche Hypothese testen wir? 5. Welche Änderung machen wir einzeln? 6. Wie messen wir Erfolg?

Diese Checkliste verhindert Aktionismus.

18. Lernaufgaben

  1. Erweitere den Cache um TTL. 2. Ergänze Metriken für Hit/Miss/eviction. 3. Simuliere einen ThreadLocal-Leak und entferne ihn. 4. Baue einen Parser für einfache GC-Pausenstatistik. 5. Vergleiche große Objektgraphen mit kleinen DTO-Projektionen. 6. Schreibe einen Incident-Bericht für steigende Heap-Baseline.
⌂ Cockpit