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:

KategorieEventWas es zeigt
CPUjdk.ExecutionSampleMethoden-Stacktraces in Intervallen — dein CPU-Profiler
Allocationjdk.ObjectAllocationSampleWelche Objekte wo alloziert werden (Sampling-basiert)
GCjdk.GarbageCollectionJede GC mit Dauer und Speicher vor/nachher
Threadsjdk.JavaMonitorEnterThreads, die auf Locks warten
Threadsjdk.VirtualThreadPinnedVirtual Thread am Carrier gepinnt — kritisch für Loom-Debugging
I/Ojdk.FileReadjdk.SocketReadDatei- und Netzwerk-I/O mit Dauer und Bytes
Exceptionsjdk.ExceptionStatisticsException-Zähler — versteckte Performance-Killer
Systemjdk.CPULoadJVM + Maschinen-CPU-Auslastung
Containerjdk.ContainerCPUThrottlingContainer-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:

  1. Automatisierte Analyse — Regel-Engine markiert auffällige Muster (lange GC-Pausen, hohe Allokation, Lock-Konkurrenz)
  2. Speicher-Tab — GC-Pausen-Zeitlinie, Allokationsraten, häufigste Objekttypen
  3. Code-Tab — Hot Methods mit aggregierten Stacktraces (Flame Graph)
  4. Threads-Tab — Aktivitätszeitlinie, blockierte Threads, Lock-Historie
  5. Event-Browser — Rohdaten jedes Events mit vollen Stacktraces

Typischer Workflow:

  1. Automatisierte Analyse durchgehen → Rote Flags identifizieren
  2. Speicher-Tab: GC-Pausen ungewöhnlich lang?
  3. Code-Tab: Welche Methoden verbrauchen die meiste CPU?
  4. Threads-Tab: Gibt es Lock-Konflikte?
  5. 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=default für Always-On (~1% Overhead)
  • maxage und maxsize begrenzen, um Disk-Volllaufen zu verhindern
  • dumponexit=true fü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.