JIT-Compiler (Just-In-Time)
Was das ist und wie es funktioniert
- JVM interpretiert zunächst den Bytecode. Hotspots (häufig aufgerufene Methoden/Schleifen) werden profiiliert und der JIT kompiliert sie in nativem Code.
- Tiered Compilation (Standard): C1 (schnell, weniger Optimierungen) → C2 (langsamerer Kompilierung, aggressive Optimierungen). Möglicherweise OSR (On-Stack Replacement) – “Umsiedlung” eines Hotspots auf den kompilierten Code mitten in der Ausführung.
- Wichtige Optimierungen: Inlining (Einfügen von Methodenaufrufen), Escape Analysis (Layout auf Felder/Skalierung, Stack-Platzierung), Engspezialisierte Intrinsics (
System.arraycopy,Math.*), Loop Unrolling, Vectorization.
Wie man beobachtet und verwaltet
Flags (für lokale Analyse):
-XX:+PrintCompilation
-XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining
-XX:CompileThreshold=10000 # Schwellenwert für "Hotness"
-XX:TieredStopAtLevel=1..4 # Begrenzung der Tiered-Level
-XX:+DoEscapeAnalysis # normalerweise aktiviertTypische Fehler
- Vergleicht die Leistung von “kaltem” Code ohne Warmup.
- Schreibt Microbenchmarks ohne JMH ⇒ misst JIT/GC, nicht den Code.
- Pessimiert den JIT: Häufige virtuelle Aufrufe, keine finalen Klassen/Methoden, übermäßige Abstraktion behindern Inlining.
Best Practices für JIT
- Warmup des Codes vor Messungen (oder verwende JMH).
- Mache wichtige Hotspots klein und “inline-friendly”.
- Vermeide unnötiges Boxing/Unboxing in Hotspots, besonders in
Streammit automatischem Packaging. - Cache, reduziere Polymorphismus dort, wo Inlining wichtig ist (sealed/final).
JVM-Speicher: Heap, Stack, Metaspace (+ weitere Bereiche)
Speicherkarte
- Heap – Objekte. Unterteilt in Young (Eden + Survivor S0/S1) und Old. Schnelle Zuweisung über TLAB (Thread-Local Allocation Buffer) und “bump-the-pointer”.
- Stack – Call Frames für jeden Thread: lokale Variablen, Referenzen, Rücksprungadresse.
- Metaspace – Metadaten der Klassen (Ersatz für PermGen seit Java 8). Wächst im nativeen Speicher.
Wird durch
-XX:MaxMetaspaceSizekontrolliert. - Code Cache – Kompilierter JIT-Code (
-XX:ReservedCodeCacheSize). - Direct memory (off-heap NIO) – wird vom OS verwaltet (
-XX:MaxDirectMemorySize).
Schnelle Konfiguration der Größen
-Xms2g -Xmx2g # min/max heap
-XX:MaxMetaspaceSize=512m # obere Grenze für Metaspace
-XX:ReservedCodeCacheSize=256mGitter-Garbage Collector (sehr kurz)
- G1 (Standard in 17+) – regional, vorhersehbare Pausen, guter Standard.
- ZGC / Shenandoah – niedrige Pausen, große Heaps, fortschrittlich (wenn minimale Latenz benötigt wird).
- Allgemeine Konzepte: Stop-the-World, Safepoints, Card Table, Write Barriers.
Typische Fehler
- Kleiner Heap ⇒ häufige GC-Pausen, OutOfMemoryError.
- Lecks durch statische Collections/Caches oder Listener (Referenzen werden nicht entfernt).
- Berücksichtigen nicht off-heap (DirectByteBuffer, Speicher für native Treiber) und geben den Heap die Schuld.
Best Practices für Speicher
- Beginne mit G1, stelle sicher, dass die Heap-Größe angemessen ist (Minimum = Maximum für Stabilität).
- Überwache Metaspace und Direct memory.
- Verwende Profiling von Allocations; reduziere kurzlebige Objekte im Hotspot-Code.
- Für Caches – soft/weak Referenzen nach Bedarf, begrenze die Größe.
Diagnosewerkzeuge
1. jconsole (einfache Telemetrie über JMX)
- Start:
jconsole→ Prozess auswählen (lokal) oder über JMX verbinden. - Was man beobachten sollte:
- Memory: Diagramme für Heap/Metaspace, Button Perform GC (nur für Diagnose).
- Threads: Thread-Zustände, Detect Deadlock.
- MBeans: Parameter der Anwendungen/Pools (wenn exponiert).
- Wann nützlich: Erste Annäherung – Heap-Wachstum, GC-Anzahl, Deadlock in einem Klick.
2. jvisualvm (GUI-Profiler/Dump-Analyse)
- Start:
jvisualvm(oder VisualVM als separate Installation). - Funktionen:
- Sampler CPU/Memory (weniger Overhead) und Profiler (genauer, aber schwerfälliger).
- Öffne heap dump (
*.hprof), schaue dir Top Consumers (wer frisst Speicher) an, inspiziere die Reachability-Graph. - Plugins: VisualGC (motivierender Heap-Generationen-Graph).
- Wann nützlich: Lecks finden, Hotspots, Hypothesen vor/nach Fixen bestätigen.
3. jmap (Heap-Snapshot und Histogramm)
Grundlegende Befehle:
jmap -histo <PID> # Top Klassen nach Anzahl/Größe
jmap -histo:live <PID> # Nur lebende Objekte (nach GC)
jmap -dump:format=b,file=heap.hprof <PID> # Heap-Dump erstellen- Verwendung: Siehe, was der Heap jetzt belegt; lade den Dump herunter und öffne ihn in VisualVM/YourKit/JProfiler.
4. jstack (Thread-Dump)
jstack -l <PID> # Thread-Dump mit Monitoren
jstack -m <PID> # Stack mit nativem CodeWas man suchen sollte:
- Threads in BLOCKED (Monitor/Lock), WAITING/TIMED_WAITING (Warten), RUNNABLE (CPU).
- Zyklische Locks (Deadlock): Am Ende des Dumps befindet sich normalerweise ein Abschnitt “Found one Java-level deadlock”.
- “Hot thread”: Thread, der ständig in RUNNABLE mit derselben Trace ist.
Nützliche Ergänzung (nicht angefordert, aber ein Muss):
jcmd <PID> VM.native_memory summary– Zusammenfassung für native/Metaspace;jcmd <PID> GC.heap_info | GC.class_histogram | Thread.print.
Mini-Fallstudien (Code → wie man diagnostiziert)
1. Speicherleck durch statische Liste
public class Leak {
private static final List<byte[]> bag = new ArrayList<>();
public static void main(String[] args) throws Exception {
while (true) {
bag.add(new byte[1_000_000]); // 1 MB
Thread.sleep(50);
}
}Diagnose:
jconsole→ Heap wächst schrittweise, GC hilft nicht → Verdacht auf Retention.jmap -histo:live <PID>→ vielebyte[].jmap -dump:format=b,file=heap.hprof <PID>→ in VisualVM öffnen, GC Roots finden → siehst das statische FeldLeak.bag. Fix: Cache begrenzen/bereinigen, WeakReference/SoftReference bei Bedarf verwenden.
2. Deadlock aufgrund von Lockreihenfolge
class Locks {
private final Object a = new Object();
private final Object b = new Object();
void m1() { synchronized (a) { sleep(100); synchronized (b) {} } }
void m2() { synchronized (b) { sleep(100); synchronized (a) {} } }
}Diagnose:
jconsole→ Threads → Detect Deadlock.jstack -l <PID>→ Abschnitt “Found one Java-level deadlock” mit Auflistung der Threads und Monitor-Eigentümer. Fix: Strikte Lockreihenfolge (immeradannb), odertryLock/Timeout, oder größere Lock-Synchronisation.
3. Hohe CPU eines Threads
Symptom: 100% CPU-Kerne. Diagnose:
top/htop→ PID → tid des Threads mit hoher CPU.jstack <PID>mehrmals hintereinander → dieselbe Trace in RUNNABLE ⇒ Hotspot.- Dann jvisualvm CPU Sampler/Profiler zur Bestätigung.
Checkliste für Problembehebung (schnell)
- Speicher:
jconsoleDiagramme →jmap -histo:live→ Heap-Dump → VisualVM. - Pausen/GC: GC-Logs aktivieren (
-Xlog:gc*in 17+), Collector/Heap-Größe prüfen. - CPU:
jstackmehrmals hintereinander → Hotspot →jvisualvmSampler. - Locks:
jconsoleDetect Deadlock →jstack -l. - Native/Metaspace:
jcmd VM.native_memory summary,-XX:MaxMetaspaceSize.
Best Practices (Zusammenfassung)
- Für Produktion: G1, angemessenes
-Xms = -Xmx, GC-Logs (-Xlog:gc*:file=gc.log). - Für Analyse: VisualVM/
jconsolefür erste Symptome, jmap/jstack für Fakten. - Reduziere Allocation an Hotspots, vermeide unnötiges Boxing und temporäre Objekte.
- Entwerfe Locks: Reihenfolge, Timeouts,
Lock/StampedLockfür Lesen. - Decke Microbenchmarks mit JMH ab, nicht mit selbstgeschriebenen Timern.
- Achte auf Metaspace/Code Cache/Direct memory – auch diese können zu OOME führen.
- Für Caches – Limits und weak/soft Referenzen bei Bedarf.