flight recorder
JFR events, always-on profiling, and mission control.
- chapter
- chapter 15.03
- reading time
- 9 min read
JDK Flight Recorder (JFR) is an event recorder built into the JVM. it logs what the JVM and your code are doing (GC pauses, lock waits, allocations, method samples, file and socket I/O, your own custom events) into a compact binary .jfr file, cheaply enough to leave on in production: the design target was at most 1% overhead with the default settings (JEP 328, java 11).
read the file with the jfr command line tool or JDK Mission Control (JMC), or consume events live with event streaming.
the picture
a plane's flight recorder doesn't wait for you to suspect a crash. it records everything, all the time, into a fixed amount of memory, and when something goes wrong, you pull the box and replay the last few minutes.
JFR is that for the JVM. events go into thread-local buffers (no locking on the hot path), get flushed to a ring of disk chunks, and old chunks fall off the end. the JVM already knows when a GC starts or a thread blocks; JFR just writes it down, with a timestamp, a thread and usually a stack trace. compare that with a profiler that attaches from outside, or with adding log lines and redeploying.
hover a stage.
record one
two ways in. at startup:
$ java -XX:StartFlightRecording=filename=shop.jfr,settings=profile -cp out Shop
[0.320s][info][jfr,startup] Started recording 1. No limit specified, using maxsize=250MB as default.
[0.320s][info][jfr,startup]
[0.320s][info][jfr,startup] Use jcmd 44527 JFR.dump name=1 to copy recording data to file.or on a JVM that's already running, with no flags set in advance (the jdk toolbox):
$ jcmd <pid> JFR.start duration=60s filename=app.jfr settings=profile
$ jcmd <pid> JFR.dump name=1 filename=now.jfr # snapshot whatever is buffered
$ jcmd <pid> JFR.stop name=1settings= picks a .jfc file of event settings. the JDK ships two: default (the low-overhead, leave-it-on one) and profile (more events, more samples, for a debugging session). you can override single settings inline: the recording below used jdk.JavaMonitorEnter#threshold=1ms to log every lock wait over 1 ms.
the program is a little order-processing app: three worker threads that parse CSV, sort orders and format a report, and then each record a checksum inside one synchronized block, plus a reporter thread that sleeps. here's what six seconds looked like:
$ jfr summary shop.jfr
Version: 2.1
Chunks: 1
Duration: 6 s
Event Type Count Size (bytes)
=============================================================
jdk.JavaMonitorEnter 2501 55754
jdk.ObjectAllocationSample 1759 26681
jdk.ExecutionSample 1082 11868
jdk.GarbageCollection 66 1323
jdk.ThreadSleep 12 251
...196 event types were registered in that recording; 84 of them actually fired.
jfr summary, log scale. thousands of lock waits (this run logged every wait over 1 ms) and samples, a few dozen GCs. a 10-second recording of the same app with the profile settings came to 3 MB.read it
$ jfr print --events jdk.JavaMonitorEnter shop.jfr
jdk.JavaMonitorEnter {
startTime = 13:01:09.471 (2026-10-10)
duration = 1.73 ms
monitorClass = java.lang.Object (classLoader = bootstrap)
previousOwner = "worker-1" (javaThreadId = 30)
eventThread = "worker-2" (javaThreadId = 31)
stackTrace = [
Shop.record(long) line: 41
Shop.lambda$main$0(int, long) line: 64
...
]
}a thread had to wait to enter a synchronized block. this run used a 1 ms threshold, and logged 2,500 of them, averaging 1.73 ms: three workers queueing on one lock. jfr view contention-by-site names the exact line.
jfr print shows it. the purple is time spent waiting for the lock; GC pauses are the amber lines.see the pattern? each worker runs (samples), then waits for the lock (purple), then runs again. a third of each worker's time is waiting. you'd never guess that from CPU usage, which looks fine, because a blocked thread uses none.
you don't have to eyeball it. jfr view has dozens of canned reports:
$ jfr view contention-by-site shop.jfr
Contention by Site
StackTrace Count Avg. Max.
--------------------------------------------------------- ----- ------- -------
Shop.record(long) 2,500 1.73 ms 7.53 ms
$ jfr view hot-methods shop.jfr
Java Methods that Execute the Most
Method Samples Percent
--------------------------------------------------------- ------- -------
Shop.record(long) 493 45.56%
java.util.Formatter$FormatSpecifier.precision(String,...) 66 6.10%
java.util.Formatter$FormatSpecifierParser.parseFlag() 51 4.71%
...(hot-methods blames Shop.record for 46% of samples because, inside the lock, the method spins for 2 ms to simulate work.) other good ones: gc-pauses, allocation-by-site, exception-by-site, pinned-threads for virtual threads, container-configuration in a container. jfr print --events <type> dumps raw events, and jfr print --json gives you something to script against.
your own events
the JVM's events tell you what the runtime did. your events can say what the application did, on the same timeline: a checkout, a cache miss, a request. an event is a class that extends jdk.jfr.Event:
import java.nio.file.*;
import jdk.jfr.*;
import jdk.jfr.consumer.*;
@Name("shop.Checkout")
@Label("Checkout")
@Category("Shop")
class CheckoutEvent extends Event {
@Label("Items") int items;
@Label("Total") double total;
}
void main() throws Exception {
Path file = Files.createTempFile("shop", ".jfr");
try (Recording recording = new Recording()) {
recording.enable("shop.Checkout");
recording.start();
for (int i = 1; i <= 3; i++) {
CheckoutEvent e = new CheckoutEvent();
e.begin();
e.items = i;
e.total = i * 10.5;
Thread.sleep(20); // "work"
e.commit();
}
recording.stop();
recording.dump(file);
}
for (RecordedEvent e : RecordingFile.readAllEvents(file)) {
IO.println(e.getEventType().getName() + ": items=" + e.getInt("items")
+ " total=" + e.getDouble("total") + " took at least 20 ms: " + (e.getDuration().toMillis() >= 20));
}
Files.delete(file);
}shop.Checkout: items=1 total=10.5 took at least 20 ms: true shop.Checkout: items=2 total=21.0 took at least 20 ms: true shop.Checkout: items=3 total=31.5 took at least 20 ms: true
this one needs a real JVM (files, the network or a database), so it doesn't run in the browser. copy it into java Main.java to try it.
begin() and commit() give it a duration; the fields become columns; @Name, @Label and @Category are what JMC shows. when the event type isn't enabled, commit() costs next to nothing, so instrumented code can stay instrumented. this program also starts and reads a recording entirely from java, with the Recording and RecordingFile APIs.
streaming
since java 14 (JEP 349), you don't have to wait for a file: RecordingStream delivers events to callbacks while the program runs, a second or so after they happen. that turns JFR into a metrics source:
import java.time.Duration;
import jdk.jfr.consumer.RecordingStream;
static volatile byte[] keep; // so the JIT can't optimize the allocations away
void main() throws Exception {
try (var rs = new RecordingStream()) {
rs.enable("jdk.GarbageCollection");
rs.enable("jdk.CPULoad").withPeriod(Duration.ofMillis(500));
rs.onEvent("jdk.GarbageCollection", e ->
IO.println("GC " + e.getString("name") + " took " + e.getDuration().toNanos() / 1000 + " µs"));
rs.onEvent("jdk.CPULoad", e ->
IO.println("cpu: jvm " + Math.round(e.getFloat("jvmUser") * 100) + "%"));
rs.startAsync();
long bytes = 0;
long end = System.currentTimeMillis() + 1500;
while (System.currentTimeMillis() < end) { keep = new byte[64 * 1024]; bytes += keep.length; } // make garbage
IO.println("allocated " + bytes / (1024 * 1024) + " MB");
}
}this one needs a real JVM (an incubator or JDK-internal module), so it doesn't run in the browser. copy it into java Main.java to try it.
no output under it because it changes every run. one run printed about 30 lines like GC G1New took 546 µs, a cpu: jvm 4% (that's a fraction of the whole 14-core machine), and the total allocated. the volatile field matters: the first version of this demo stored nothing, the JIT removed the allocations as dead code, and the loop "allocated" three million megabytes in 1.5 seconds. (JMH has a whole chapter on that trick.)
new in jdk 25
three JFR JEPs landed in 25:
- method timing and tracing (JEP 520): name methods with a filter and JFR instruments them, no agent required. real run on the shop app:
$ java '-XX:StartFlightRecording:method-timing=Shop::formatReport;Shop::sortByTotal;Shop::parseCsv,filename=timing.jfr' -cp out Shop 4000
$ jfr view method-timing timing.jfr
Method Timing
Timed Method Invocations Minimum Time Average Time Maximum Time
---------------------------- ----------- ------------ ------------ ------------
Shop.formatReport(List) 3,200 1.020000 ms 2.080000 ms 77.300000 ms
Shop.parseCsv(List) 3,200 0.323000 ms 0.624000 ms 23.700000 ms
Shop.sortByTotal(List) 3,200 0.322000 ms 0.574000 ms 17.200000 msmethod-trace= records every single call with a stack trace instead, and the filter can also be an annotation (@jakarta.ws.rs.GET) or a whole class.
- cooperative sampling (JEP 518): the execution sampler used to walk a suspended thread's stack at any instruction, which was fragile. now it records the program counter, lets the thread reach its next safepoint, and reconstructs the stack there, correcting for the bias that would otherwise introduce. more in profiling.
- CPU-time profiling (JEP 509, experimental, linux only):
jdk.CPUTimeSamplesamples by CPU time used rather than wall-clock time, including time spent in native code. on this mac the event type exists but records nothing.
mission control
JDK Mission Control is the GUI for .jfr files: a separate download, open source in the OpenJDK project. open a recording and you get an automated analysis page ("rules" that flag lock contention, GC pressure, exception storms, heap growth), then timelines per thread, a flame-graph view of the samples, allocation pressure by class, and every event in a table you can filter and group. the jfr command line covers most of the same ground for a terminal, and JMC is nicer when you don't know yet what you're looking for.
- JFR is a flight recorder inside the JVM: events with timestamps, threads and stacks, cheap enough for production.
- start it with
-XX:StartFlightRecordingorjcmd <pid> JFR.start. give it a filename. jfr summary,jfr view <report>andjfr printread recordings; JMC is the GUI.- your own events extend
jdk.jfr.Eventand land on the same timeline. - samples only show running threads. waiting has its own events: look at both.