Java Flight Recorder (JFR) is a low-overhead event-recording framework that is integrated into the HotSpot JVM. It continuously records CPU, memory, garbage collection, thread, lock, and I/O events so that you can understand what happened in your production system.
In this article, we explain the essential mechanism of JFR, the situations in which it should be used, and how to use it in real applications. By the end, you should have a solid understanding of JFR and a practical example that you can use right away.
What Is Java Flight Recorder?
JFR is a profiling/diagnostics tool that comes standard with the JVM. Built into HotSpot, no special agents or custom builds required. Turn it on, and it will save tons of event data to .jfr files. Method sampling, GC timeouts, thread state changes, I/O operations… everything is covered.
This is why the flight recorder analogy fits perfectly. Instead of having to worry about reproducing the problem with a debugger, you can let JFR record it continuously and later look at the exact moment the problem occurred.
Why JFR Was Created
Traditional Java profilers utilize dynamic bytecode instrumentation along with external Java agents. However, such profilers introduce unacceptable overhead when run in a production environment. They add significant overhead to the execution of methods, thus creating long tail latency spikes not present in the application code under investigation and impacting safepoint polling overhead and transaction throughput.
Taking point-in-time diagnostics such as thread dumps or heap dumps only captures the state of the system at a given moment. They cannot be used to answer questions about the timeline of events leading up to a memory allocation or CPU contention issue. By instrumenting the HotSpot C++ engine and writing to thread-local memory regions before publishing them to a global ring buffer, JFR captures a continuous stream of events with less than 2% CPU overhead.
A Brief History of JFR
In fact, JFR has an older history than OpenJDK. It was originally born in BEA’s JRockit virtual machine. Since then, the JVM has been famous for its diagnostic capabilities in production environments. Later, when both BEA and Sun Microsystems were acquired by Oracle and the JRockit and HotSpot development teams were merged, JFR was ported to HotSpot and became a commercial feature.
The use of JFR in production required a commercial license for a long time. The turning point came with the release of JDK 11. Oracle open-sourced JFR via JEP 328 and included it in OpenJDK. Since then, this diagnostic tool has been freely available as a standard feature in all OpenJDK distributions, and is still actively maintained by the development team as a core feature rather than an add-on.
How Java Flight Recorder Works
JFR combines a JVM-embedded event system, thread-local buffering, and lightweight sampling to record detailed runtime behavior with minimal performance cost.
Event-Based Monitoring
At the core of JFR is a complex event system. Hundreds of event collection points are built into the JVM and the Java class libraries. When a noteworthy situation occurs, such as when a class load, lock conflict, or object allocation exceeds a threshold, an event record is created with a timestamp and associated metadata.
These events are first written to the thread’s local buffer and periodically flushed to shared memory. When you stop recording (or when the set time is reached), the recorded data is serialized into a single .jfr file that can be opened later for filtering and analysis.
Types of Events Captured
The events that JFR captures mainly fall into the following categories: CPU usage and method sampling, memory and object allocation, garbage collection, thread state and race, lock control events, file and network I/O, exception information, and class loading status. Each class is individually configurable, and this flexibility allows the JFR to handle everything from light resident surveillance to in-depth special investigations.
How JFR Maintains Low Overhead
Even with the full analysis configuration in place, JFR has a single-digit percentage impact on performance. Several important design inputs contribute to this:
Concretely, the default jfc profile keeps overhead around 1%, while the more detailed profile.jfc template, which adds memory allocation tracking and thread lock profiling, raises it to roughly 2-5%.
- Use sampling instead of full instrumentation: Avoid the burden of instrumentation for each method call by taking regular call stack snapshots to gather method-level information.
- Native integration at the JVM level: JFR is built directly into HotSpot instead of rewriting the bytecode, which avoids the overhead of calling Java agents
- Thread local buffer: Events are first written to the memory area dedicated to each thread. This prevents threads from competing for locks and prevents system-wide delays.
- Configurable Event Filtering: If you only need light storage, you can disable or limit expensive event types such as memory allocation monitoring.
Why Developers Use JFR
JFR has an important place in the developer’s toolkit because it can solve almost any JVM performance problem you encounter on a daily basis.
1. Investigating High CPU Usage
Use method sampling data to determine which methods are consuming CPU resources. In addition, it is possible to determine whether the load is distributed over several threads or concentrated on a specific hot path.
Keep in mind that JFR’s method sampling is safepoint-biased, which means that sampling only occurs at safepoints. Profiling tools based on AsyncGetCallTrace, such as async-profiler, can perform sampling at arbitrary points, providing more accurate information about CPU-intensive call stacks.
2. Diagnosing Memory Problems
JFR monitors object allocations by intercepting Thread Local Allocation Buffer allocations (jdk.ObjectAllocationInNewTLAB and jdk.ObjectAllocationOutsideTLAB). This allows identifying methods that lead to the highest number of allocations and, consequently, to frequent GC pauses. To find the slow memory leaks without spending a lot of time and memory on a full heap dump, check the jdk.OldObjectSample event. JFR samples a small number of objects that survived several garbage collection cycles and keeps track of them. By following the references to the GC root, JFR can detect a memory leak trace in the .jfr file. Thus, it becomes possible to see which collection or static field holds the unreferenced object.
3. Analyzing Garbage Collection
JFR records the cause and duration of the GC each time it occurs. You can directly understand the root cause of downtime and performance issues without having to analyze large GC logs.
4. Finding Thread Contention
Because thread state transitions (running, waiting, blocking) are continuously recorded, it’s easy to see where threads are stuck and holding each other up.
5. Detecting Lock Contention
Monitors and lock events show which objects are in contention and how long threads are waiting to acquire a lock. This is very useful for troubleshooting issues where the service seems to “hang” during heavy loads.
The underlying jdk.JavaMonitorEnter event contains the monitor class, the waiting thread, and the blocking thread, allowing you to easily determine the exact object that leads to the deadlock.
6. Investigating I/O Bottlenecks
File and socket I/O events reveal slow read, write, and network calls. By using CPU profiles alone, you can find performance limitations that are often overlooked.
7. Performance Regression Analysis
Because recording costs are so low, you can compare recordings from stable and problematic releases. Just like playing a “spot the difference” game, you can pinpoint the cause of the performance degradation.
What Information Can JFR Capture?
Let’s take a closer look at these event classifications that determine the practical value of save files:
- CPU events: periodically collected call stack samples that reveal how your CPU time is being used
- Memory events: Object allocation information, TLAB usage, heap memory snapshots, etc.
- Garbage Collection Events: Detailed information about each GC pause time, the reason for the event, the time required by each step, and the heap usage before and after the collection.
- Thread Activity Monitoring: Monitors thread start/stop records, state transition processes, and wait/stop states.
- Lock race monitoring: Records monitor entry and exit events and lock race duration.
- File system operations: Records read and write operations of files and their processing times.
- Network communication monitoring: Monitors socket read and write activity.
- Exception Event Statistics: Records exceptions made, error information, and how often they occur.
- JVM Runtime Events: Captures class loading process, JIT compilation activity, and JVM parameter configuration information.
When Should You Use JFR?
JFR fits any scenario where you need continuous, low-overhead visibility into JVM behavior, from production monitoring to load testing and capacity planning.
1. Production Performance Issues
Its uptime is very low, making it ideal for continuous use in production environments. Field data can be obtained as soon as a problem occurs, saving the trouble of repeating the problem.
2. Load Testing
Enabling logging during load testing allows bottlenecks (GC load, lock contention, slow I/O, etc.) to be detected before real traffic arrives.
3. Performance Tuning
Provides complete benchmarks before and after tuning when adjusting GC parameters, thread size, and JIT compilation settings.
4. Regression Testing
By comparing the saved data of different builds, it is possible to identify performance deterioration trends in advance before a new build is released.
5. Capacity Planning
The actual memory allocation and processor utilization trends can be extrapolated from the recorded data over time, which may allow more accurate capacity planning than rule-of-thumb methods.
When JFR May Not Be the Best Choice
Although it is an amazing multipurpose tool for getting insight into JVM-level performance, it is not universal.
1. Continuous Method-Level Profiling
If there is a need to analyze the application method-level performance continuously and frequently, a dedicated profiler is a better choice than the general-purpose sampling flight recorder
2. Native Memory Analysis
It mainly collects data about the Java cache, so to troubleshoot native memory allocation and off-heap allocations, it is necessary to use the Native Memory Monitoring Tool.
3. Heap Leak Investigation
Suspicious memory allocation patterns can be detected, but a full heap dump and professional heap analysis tools are usually required to identify actual memory leaks and trace object reference chains.
4. Crash Diagnostics
If the JVM has already crashed, JFR cannot respond afterwards unless autosave on exit is specified. In that case, we rely on the hs_err logs and core dump files to analyze the cause of the crash.
How to Start a JFR Recording
There are several ways to start recording, depending on whether you want to start from scratch or add it to an already running program. yCrash’s guide on how to capture a JFR recording.
1. Starting at JVM Startup
Enable the JFR function as soon as the JVM starts by specifying the command line arguments:
java -XX:StartFlightRecording=settings=profile,delay=15s,duration=300s,filename=startup.jfr,dumponexit=true -jar app.jar
This method is ideal if you want to save the initialization process itself, such as class loading or JIT warmup.
Essential Parameter Breakdown
- settings=profile: swaps the default light-weight profiler with the sampling one, enabling the memory allocations tracking and thread locks profiling.
- delay=15s: prevents the application from profiling its start-up phase (typically, when using the framework such as Spring).
- dumponexit=true: ensures that the ring-buffer is dumped to the disk when the application is stopped.
Production Tip: configure your recording to be always-on. Use settings=default,disk=true,maxage=6h,maxsize=5g to record a continuous stream of allocation events. This way, you will have a six-hour window of JVM telemetry available for analysis without consuming too much disk space (the size is limited to five gigabytes). Note that in a containerized environment, you might want to bind-mount the recording directory into a persistent volume so that it is not deleted when the container is re-created. Similarly, make sure that dumponexit=true so that you can capture the state of the application before it is killed by the orchestrator (e.g., Kubernetes).
2. Starting on a Running JVM
If the program is already running, you can start recording without restarting them by attaching to the process with the jcmd tool:
jcmd <pid> JFR.start name=JFRDemo settings=profile duration=300s filename=jfr-demo.jfr
Fig: jcmd used to start a JFR recording on a running JVM
Specify the settings=profile option here to enable a more detailed set of events. It is suitable for short-term recording of 300 seconds, but is a bit heavy for long-term use.
3. Recording Through Java Mission Control
Java Mission Control (Java Mission Control) provides a graphical interface to start, stop and display recordings without typing commands in the console. Attach to a running JVM over JMX and define the event patterns of interest in a UI before starting your recording.
4. Continuous Recording
When you are interested in continuous recording to have information about your application state over an extended period, using Java Flight Recorder usually involves allocating a ring buffer for JFR storage. It is especially the case if you want the recording to be cyclic, which means that it will overwrite itself when it reaches the end of the buffer. You can set the buffer size or recording time so that the recording does not consume more space on your disk than necessary. For example, you can record your application for several hours while only retaining the last few hours of valuable data.
Practical JFR Example
Let’s now take a practical example. For this, let’s create relevant data for a sample application that we will want to analyze using JFR to find out what is causing it to have problems with CPU or memory.
Test Application
Our Spring Boot test application has 3 endpoints that will allow us to generate 3 types of load that will affect certain aspects of the JVM.
@GetMapping("/cpu")public Map<String, Object> generateCpuLoad() { long result = 0; for (long i = 0; i < 800_000_000L; i++) { result += (i * 31) % 7; } return Map.of("test", "cpu", "result", result);}@GetMapping("/memory")public Map<String, Object> generateAllocationPressure() { List<byte[]> objects = new ArrayList<>(); for (int i = 0; i < 180; i++) { objects.add(new byte[512 * 1024]); } return Map.of("test", "memory", "allocatedObjects", objects.size());}private final Object sharedLock = new Object(); @GetMapping("/lock")public Map<String, String> generateLockContention() throws InterruptedException { synchronized (sharedLock) { Thread.sleep(2_000); } return Map.of("test", "lock", "status", "completed");}
The /cpu endpoint creates a sustained hotspot, the /memory endpoint generates temporary byte[] allocation pressure, and concurrent calls to /lock force threads to compete for the same monitor.
Starting the Recording
With the application running, start recording from the command line:
jcmd <PID> JFR.start name=JFRDemo settings=profile duration=300s filename=jfr-demo-2.jfr
Since this is a 5-minute spot check, I chose profiling settings. This is for intensive analysis rather than continuous monitoring.
Generating the Workload
While the recording was active, we invoked all three endpoints repeatedly from PowerShell:
1..8 | ForEach-Object { Start-Job { Invoke-RestMethod http://localhost:8080/api/cpu } }1..10 | ForEach-Object { Invoke-RestMethod http://localhost:8080/api/memory }1..10 | ForEach-Object { Start-Job { Invoke-RestMethod http://localhost:8080/api/lock } }Get-Job | Wait-Job | Receive-JobGet-Job | Remove-Job
CPU, memory, garbage collection, and chain lock contention were all logged in this coordinated attack.
Reviewing the Recording
When the recording time is up, first check the content with a simple command:
jfr summary jfr-demo-2.jfr
Fig: Output of jfr summary showing the recorded event counts.
This log records 1007 execution samples, 311 memory allocation samples, 72 garbage collections, 8 monitors, and 772 thread breaks. It’s a rich data set covering CPU, memory, GC and locking behavior, so it’s perfect for future analysis.
Understanding the JFR File
The .jfr file is a compact, self-describing binary format whose size and retention depend entirely on the event settings and recording duration you choose.
1. What’s Inside a .jfr File?
A .jfr file is a compact, self-describing binary format. In addition to the raw event data we talked about earlier, it also contains metadata that describes the schema of each event type. In this way, the user can read the stored content without knowing in advance which events are in use on the tool side.
2. Recording Size
The size of the recorded file varies greatly depending on the settings and recording time. Short-term saves with default settings are only a few megabytes, but longer saves using profile templates to track detailed memory usage can be much larger. If you use a ring buffer configuration, you can keep the size small even during continuous recording.
3. Recording Duration
There is no upper limit to the recording time of JFR. Some teams even enable JFR for the lifetime of the service. The most important thing is to choose event settings and buffer size that fit the recording time. This way you can keep useful data without consuming disk space.
How to Analyze a JFR Recording
Recordings can be explored visually in Java Mission Control, queried from the command line with jfr print, or summarized automatically with AI-based tools like yCrash.
1. Java Mission Control
JMC is a tool for manual analysis of .jfr files. The recorded contents are organized and displayed by categories such as CPU, Memory, Threads, I/O, etc. This is the best option when you want to dig deeper.
2. Command-Line Tools
The jfr print command is useful when you want to easily check or manipulate with a script. Since you can print certain types of events directly to the terminal without launching the GUI, it is a convenient way to quickly check the occurrence of a certain event.
Example:
jfr print --events jdk.GarbageCollection jfr-demo-2.jfr
3. Analysis with yCrash JFRPlayer
You can read the records manually, but it takes time and requires special skills. With automated analysis tools like yCrash, you can simply upload a .jfr file and generate a structured report that includes cause candidates, memory breakdowns, GC statistics, and more. It’s really useful when you don’t have to keep track of event information.
The run summary was automatically generated when you downloaded the demo recording. This summary showed the cause of the CPU peak usage. You can see that it is the PerformanceTestController.generateCpuLoad() method. The data shows that this method took 96.52% of CPU time and was always at the top of the call stack for all 972 samples collected.
Fig: AI-generated performance summary showing CPU bottlenecks and associated garbage collection activities
Looking at the heap view, it’s clear that temporary byte[] table allocations take up most of the pooled memory. It accounts for 76.79% of the total – as expected since the /memory endpoint repeatedly allocates 512KB of memory.
Fig: Distribution of the heap by class, showing that byte[] allocations dominate
On the garbage collection side, we observed an average pause of 1.84 ms in the KPI view, with a maximum of 20 ms. Most pauses end in 10ms – a healthy pattern given the current memory allocation pressure.
Fig: Garbage collection key performance metrics including pause time distribution
This type of automated analysis is a great first step before using JMC manually – the summaries will hone in on the problem without you having to go through each record tab yourself.
Best Practices for Using JFR
Getting the most out of JFR comes down to smart configuration choices and consistent habits, not just knowing the tool exists.
- Keep Recordings Running: The main advantage that JFR has over most profilers is that it can run for a long time. Continuous, minimal storage prevents you from losing data in the event of a problem.
- Choose Appropriate Event Settings: The default model is light enough for continuous use in a production environment. Profile templates, on the other hand, are suitable for short-term sessions where you actively monitor specific issues.
- Capture During the Incident: If you haven’t enabled continuous recording, start recording as soon as you notice the problem. Instead of waiting for a solution. Recordings started after the event cannot tell what happened before it happened.
- Avoid Excessive Custom Events: Custom application events are a powerful feature, but if you create too many instances or fire them too often, you may lose the default value that JFR is based on.
- Retain Historical Recordings: Specifically, if you archive the record file at deployment and save the history file, you can get excellent mileage out of them in terms of performing regression analysis to understand cause-effect relationships and incremental technology diffusion.
- Combine with Other Diagnostics:JFR is best used with a combination of thread dumps, heap pulls, and GC logs. It is particularly valuable in scenarios where other approaches do not provide enough information, for example, diagnosing native memory and memory leaks.
Common Mistakes
- Recording Too Late: If you start recording when the problem is already solved, you will miss the most important moment.
- Recording Too Short: Sufficient recording time is required to capture intermittent problems such as periodic GC loads or slow memory growth.
- Ignoring Thread Data: If you go directly to the CPU tab and analyze the thread status without checking, you’ll miss race and blocking issues that don’t show up in CPU usage.
- Looking Only at CPU: CPU is just one aspect. In the real world, slowdowns are often caused by GC pauses, lock contention, and I/O, and are not necessarily caused by computationally intensive code.
- Forgetting GC Analysis: GC events are logged by default, but it’s easy to end up only looking at method sampling data. However, GC pauses are a typical cause of latency spikes.
JFR vs Other JVM Diagnostic Tools
The JVM diagnostic toolkit has many different tools, each answering different questions. Let’s compare how JFR differs from other tools (see also this detailed comparison of Java Mission Control vs. yCrash JFR Player)
| Tool | What It Captures | Best For |
|---|---|---|
| JFR | CPU, memory, GC, threading, locking and I/O event information | First choice for almost all performance problems; continuous past, low overhead costs |
| Heap Dumps | Object graph at a given point in time | Check for complete memory leaks (generally analyzed with HeapHero) |
| Thread Dumps | All threads’ stack snapshot at a given moment | Application hangs and deadlock analysis (FastThread analysis is common) |
| GC Logs | Text-based GC activity record | Deep tuning; commonly used with the GCeasy tool |
| async-profiler | Native-stack-compatible CPU/Memory allocation sample with minimal overhead | Implements very detailed CPU profiling |
| JMX | Real-time MBean data | Powerful tools for building meters and monitoring dashboards |
| Native Memory Tracking | Off-heap and native Java memory growth | Memory usage traceable off the heap |
In reality, these tools do not compete with each other, but rather look at the same JVM from different perspectives. Because JFR is easy to set up and provides broad coverage, it is often used first, and once a specific problem area is identified, other tools can be used to dig deeper.
Frequently Asked Questions
1. Is JFR free?
Yes. Starting with JDK 11, JFR is fully open source and is included for free with every OpenJDK build.
2. Does JFR slow down applications?
Although there is some overhead, the default settings are usually less than 2% and acceptable for most production environments.
3. Can JFR run continuously?
Yes! Actually that is one of the cool features of JFR – it is a ring buffer so you can set a size and it will roll over when it gets full retaining the latest data.
4. Can JFR detect memory leaks?
JFR can detect some strange allocations that could point to potential leaks but you’ll still need to do a heap dump and do some analysis to find the root cause.
5. Is JFR available in OpenJDK?
Yes. It has been included in the OpenJDK standard since JDK 11 and was later ported to the updated version of JDK 8.
6. How long should a recording be?
It depends on the problem. A few minutes is enough for CPU spikes, but intermittent GC or Memory issues can require hours of recording, and that’s when ring buffer’s continuous recording comes into its own.
Conclusion
This time I gave an overview of Java Flight Recorder (JFR). We also explained in detail how the intelligent event-based architecture enables low-load monitoring and how it works with other JVM diagnostic tools such as heap dumps, thread dumps, and GC logs. In the actual demonstration, we presented the entire process from starting the recording to generating test loads and analyzing the results, and we presented both manual analysis methods and automatic processing techniques.
The real value of JFR is that it can always be used with peace of mind. When a problem occurs in production, you no longer need to shuffle the event to replay- valuable performance data is already silently recorded. As you use it more and more, you will learn about event settings and be able to combine the best auxiliary tools for each situation, and this seemingly unassuming profiler will start to shine more than you imagined and will no longer sit in the corner of the toolbox.

