RU | EN | DE

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 aktiviert

Typische 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 Stream mit 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:MaxMetaspaceSize kontrolliert.
  • 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=256m

Gitter-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 Code

Was 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:

  1. jconsole → Heap wächst schrittweise, GC hilft nicht → Verdacht auf Retention.
  2. jmap -histo:live <PID> → viele byte[].
  3. jmap -dump:format=b,file=heap.hprof <PID> → in VisualVM öffnen, GC Roots finden → siehst das statische Feld Leak.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 (immer a dann b), oder tryLock/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)

  1. Speicher: jconsole Diagramme → jmap -histo:live → Heap-Dump → VisualVM.
  2. Pausen/GC: GC-Logs aktivieren (-Xlog:gc* in 17+), Collector/Heap-Größe prüfen.
  3. CPU: jstack mehrmals hintereinander → Hotspot → jvisualvm Sampler.
  4. Locks: jconsole Detect Deadlock → jstack -l.
  5. 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/jconsole fü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/StampedLock fü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.