Deine JVM-Anwendung läuft langsam, aber du hast keinen Profiler? Oder schlimmer: Sie läuft produktiv, und jeder externe Profiler würde sie unbrauchbar verlangsamen? Genau dafür wurde Java Flight Recorder (JFR) entwickelt — ein in die JVM integrierter, kontinuierlicher Profiler mit weniger als 1% Overhead.
Dieser Artikel zeigt, wie du JFR einsetzt, eigene Events definierst und mit JDK Mission Control analysierst.
Was ist JFR?
JFR ist ein ereignisbasiertes Profiling-Framework, das seit JDK 11 in OpenJDK enthalten ist (vorher kommerziell). Es zeichnet Hunderte von Event-Typen auf: GC-Pausen, Thread-Aktivität, Lock-Konflikte, I/O, Allokation, Exceptions und vieles mehr.
Zwei Modi:
- Kontinuierliche Aufzeichnung mit Ringpuffer — wie eine Dashcam; im Problemfall rufst du die letzte Stunde ab
- Gezielte Aufzeichnung mit Profil-Einstellungen — detailliertere Events, etwas mehr Overhead (~2%)
JFR aus der Kommandozeile starten
Der einfachste Einstieg: -XX:StartFlightRecording beim JVM-Start:
<em># 60 Sekunden aufzeichnen, Datei beim Beenden schreiben</em>
java -XX:StartFlightRecording:filename=meine-app.jfr,duration=60s \
-jar meine-app.jar
Code-Sprache: HTML, XML (xml)
Für Produktion empfiehlt sich die kontinuierliche Aufzeichnung mit Ringpuffer:
java \
-XX:StartFlightRecording:name=production,maxage=1h,maxsize=500MB,settings=default \
-jar meine-app.jar
Code-Sprache: JavaScript (javascript)
Tritt ein Problem auf, dumpst du den aktuellen Puffer ohne Neustart:
jcmd <PID> JFR.dump name=production filename=problem.jfr
Code-Sprache: HTML, XML (xml)
jcmd erlaubt vollständige Laufzeit-Kontrolle:
jcmd <PID> JFR.start name=debug settings=profile duration=120s filename=debug.jfr
jcmd <PID> JFR.check <em># Laufende Aufzeichnungen anzeigen</em>
jcmd <PID> JFR.stop name=debug <em># Aufzeichnung beenden</em>
Code-Sprache: HTML, XML (xml)
Die wichtigsten Event-Typen
JFR hat über 200 Event-Typen. Hier die essenziellen für die Performance-Analyse:
| Kategorie | Event | Was es zeigt |
|---|---|---|
| CPU | jdk.ExecutionSample | Methoden-Stacktraces in Intervallen — dein CPU-Profiler |
| Allocation | jdk.ObjectAllocationSample | Welche Objekte wo alloziert werden (Sampling-basiert) |
| GC | jdk.GarbageCollection | Jede GC mit Dauer und Speicher vor/nachher |
| Threads | jdk.JavaMonitorEnter | Threads, die auf Locks warten |
| Threads | jdk.VirtualThreadPinned | Virtual Thread am Carrier gepinnt — kritisch für Loom-Debugging |
| I/O | jdk.FileRead, jdk.SocketRead | Datei- und Netzwerk-I/O mit Dauer und Bytes |
| Exceptions | jdk.ExceptionStatistics | Exception-Zähler — versteckte Performance-Killer |
| System | jdk.CPULoad | JVM + Maschinen-CPU-Auslastung |
| Container | jdk.ContainerCPUThrottling | Container-CPU-Throttling-Ereignisse |
Eigene JFR-Events definieren
Das programmierbare API (jdk.jfr) erlaubt eigene Events mit minimalem Code:
import jdk.jfr.*;
@Name("shop.BestellungAufgegeben")
@Label("Bestellung aufgegeben")
@Category({"Shop", "Bestellungen"})
public class BestellungEvent extends Event {
@Label("Bestellnummer")
String bestellnummer;
@Label("Einzelposten")
int posten;
@Label("Gesamtbetrag")
double betrag;
}
Code-Sprache: JavaScript (javascript)
Nutzung im Code:
BestellungEvent event = new BestellungEvent();
event.bestellnummer = "BO-2026-0042";
event.posten = 3;
event.betrag = 149.97;
event.commit(); <em>// Instant-Event: sofort weggeschrieben</em>
Code-Sprache: JavaScript (javascript)
Für die Messung von Laufzeiten (Duration-Event):
@Name("shop.PreisBerechnung")
@Label("Preisberechnung")
public class PreisBerechnungEvent extends Event {}
<em>// Nutzung:</em>
PreisBerechnungEvent event = new PreisBerechnungEvent();
event.begin();
<em>// ... teure Berechnung ...</em>
event.commit(); <em>// commit() impliziert end(); Duration automatisch gesetzt</em>
Code-Sprache: JavaScript (javascript)
Performance-Tipp: Baue Payload nur, wenn das Event auch aktiviert ist:
BestellungEvent event = new BestellungEvent();
event.begin();
<em>// ... Operation ...</em>
if (event.shouldCommit()) {
event.bestellnummer = bestellnummer; <em>// teure Konstruktion nur wenn nötig</em>
event.commit();
}
Code-Sprache: JavaScript (javascript)
JFR-Streaming: Echtzeit-Alerting
Seit JDK 14 kann man JFR-Events als Stream abonnieren — ideal für Alerts auf GC-Pausen oder hohe CPU-Last:
import jdk.jfr.consumer.RecordingStream;
import java.time.Duration;
public class JfrMonitor {
public static void main(String[] args) {
try (var stream = new RecordingStream()) {
stream.enable("jdk.GarbageCollection");
stream.enable("jdk.CPULoad");
stream.onEvent("jdk.GarbageCollection", event -> {
long pauseMs = event.getDuration().toMillis();
if (pauseMs > 100) {
System.out.printf("⚠️ Lange GC-Pause: %dms%n", pauseMs);
}
});
stream.onEvent("jdk.CPULoad", event -> {
float jvmCpu = event.getFloat("jvmUser");
float machineCpu = event.getFloat("machineTotal");
if (machineCpu > 0.90f) {
System.out.printf("⚠️ Hohe System-CPU: %.1f%%%n",
machineCpu * 100);
}
});
stream.startAsync();
Thread.sleep(Duration.ofHours(1).toMillis());
}
}
}
Code-Sprache: JavaScript (javascript)
Analyse mit JDK Mission Control
JDK Mission Control (JMC) ist die grafische Analyse-Umgebung. Nach dem Öffnen einer .jfr-Datei zeigt es:
- Automatisierte Analyse — Regel-Engine markiert auffällige Muster (lange GC-Pausen, hohe Allokation, Lock-Konkurrenz)
- Speicher-Tab — GC-Pausen-Zeitlinie, Allokationsraten, häufigste Objekttypen
- Code-Tab — Hot Methods mit aggregierten Stacktraces (Flame Graph)
- Threads-Tab — Aktivitätszeitlinie, blockierte Threads, Lock-Historie
- Event-Browser — Rohdaten jedes Events mit vollen Stacktraces
Typischer Workflow:
- Automatisierte Analyse durchgehen → Rote Flags identifizieren
- Speicher-Tab: GC-Pausen ungewöhnlich lang?
- Code-Tab: Welche Methoden verbrauchen die meiste CPU?
- Threads-Tab: Gibt es Lock-Konflikte?
- Event-Browser: In einzelne Events drill-down mit Stacktraces
Eine Alternative zur GUI ist das CLI-Tool jfr:
jfr summary recording.jfr <em># Event-Statistik</em>
jfr print --events jdk.GarbageCollection recording.jfr <em># GC-Details</em>
Code-Sprache: HTML, XML (xml)
Produktions-Best-Practices
<em># Empfohlene Produktions-Flags:</em>
java \
-XX:StartFlightRecording:name=prod,maxage=1h,maxsize=500MB,settings=default,dumponexit=true,filename=/var/log/jfr/exit.jfr \
-jar app.jar
Code-Sprache: PHP (php)
settings=defaultfür Always-On (~1% Overhead)maxageundmaxsizebegrenzen, um Disk-Volllaufen zu verhinderndumponexit=truefür Post-Mortem-Analyse nach Crash/OOM
Fazit
JFR ist das unterschätzteste Werkzeug des JDK. Es läuft immer, kostet fast nichts und liefert im Problemfall genau die Daten, die du brauchst — ohne dass du vorher einen Profiler installieren musst. Custom Events erlauben dir, domänenspezifische Metriken (Bestellvolumen, API-Latenzen) nahtlos in dasselbe Tool zu integrieren.
Mein Rat: Aktiviere JFR auf allen produktiven JVMs. Der erste Vorfall, bei dem dir jcmd JFR.dump die Ursache in Minuten statt Tagen zeigt, wird dich überzeugen.