If you’re running anything on the JVM (Java Virtual Machine) in production and you’ve never opened a Flight Recorder file, you’re leaving a free profiler on the table. Java Flight Recorder (JFR) is a low-overhead event recorder built into the JVM itself — no agent, no instrumentation, no extra dependency. You turn it on with a flag, you get back a .jfr file, you open it in JDK Mission Control and it tells you where your CPU time went, what the garbage collector (GC) was doing, which threads were contending on which locks, and which methods allocated the most memory. Originally a commercial JRockit feature, JFR was donated to OpenJDK as JEP 328 and has been free for production use since JDK 11. ☕
What it actually records
JFR is event-based. The JVM and the operating system fire events — GC pauses, thread state changes, exception throws, just-in-time (JIT) compilations, file I/O, socket reads — and JFR writes them to a ring buffer. You can also emit your own custom events from application code. The default profile aims for under 2% overhead, which is the magic number that makes it acceptable to leave running in production.
Quick start: capture a recording at JVM startup
The simplest way to capture a recording is to ask for one when you launch the JVM. Add -XX:StartFlightRecording to your java command:
1 | java -XX:StartFlightRecording=duration=60s,filename=myapp.jfr,settings=profile -jar myapp.jar |
This records for 60 seconds and writes myapp.jfr to the working directory. Two settings templates ship with the JDK:
- settings=default — <1% overhead, fine to leave running indefinitely
- settings=profile — <2% overhead, more sampling detail, what you want for a focused profiling session
For a longer-running “black box” recording that always has the last few hours of data on disk:
1 | java -XX:StartFlightRecording=disk=true,maxage=6h,settings=default -jar myapp.jar |
Capture a recording on a running JVM
Often you don’t want to restart the process — you want to attach to a JVM that’s already running and capture a slice. That’s what jcmd is for. First, find the process ID (PID):
1 | jcmd -l |
Then start, dump, and stop a recording on that PID:
1 2 3 4 5 6 7 8 9 10 11 | # Start a 2-minute profiling recording on PID 12345 jcmd 12345 JFR.start name=debug settings=profile duration=2m filename=/tmp/debug.jfr # Check what recordings are active jcmd 12345 JFR.check # Dump the current state of an in-progress recording without stopping it jcmd 12345 JFR.dump name=debug filename=/tmp/snapshot.jfr # Stop a recording explicitly (otherwise it stops at the duration) jcmd 12345 JFR.stop name=debug |
The JFR.dump trick is the one I reach for most often: keep a continuous low-overhead recording running, and when something interesting happens (a slow request, a memory spike), dump the in-flight buffer to a file for analysis. You get the events leading up to the moment, not just events from when you noticed.
View the result in JDK Mission Control
A .jfr file is a binary log of events — you need a viewer. The canonical one is JDK Mission Control (JMC):
- Download from Adoptium or jdk.java.net/jmc — it’s a separate download from the JDK itself.
- Open the .jfr file. JMC’s Automated Analysis Results tab is the one to read first — it scans the recording and surfaces likely problems with traffic-light severity (long GC pauses, lock contention, thread hot spots, allocation pressure).
- From there, dive into the per-area pages: Java Application for method profiling, Memory for allocations and GC, Threads for contention and parking, Environment for CPU and OS-level events.
If you just want a quick text dump without firing up a graphical interface, the JDK ships a jfr command-line tool:
1 2 | jfr summary myapp.jfr # event counts and recording metadata jfr print --events CPULoad myapp.jfr # print all events of a given type |
IntelliJ IDEA Ultimate also opens .jfr files directly with a built-in viewer if you’d rather not install JMC.
When to reach for it
JFR shines for the questions that a debugger or a print statement can’t answer cheaply:
- “Why does p99 latency spike every few minutes?” (p99 = the slowest 1% of requests) — look at the GC Pauses view.
- “Which method is the CPU actually spending time in?” — Method Profiling sampler view.
- “Why is this app allocating so much?” — Allocation view, grouped by class and stack trace.
- “Are my threads stuck?” — Thread Park and Java Monitor Wait events.
It’s not a replacement for application performance monitoring (APM) tools like Datadog or New Relic, or for distributed tracing — JFR sees a single JVM, not a whole fleet. But for a deep look at what one process is doing, with overhead low enough to leave it running in production, nothing else in the Java ecosystem comes close. 🚀