Flight Recorder is the most powerful, low-overhead profiling tool built into the JDK, yet most Java developers never even touch it.
Let’s see it in action. Imagine we have a simple Java application that’s performing poorly. We want to understand where it’s spending its time.
import java.util.concurrent.TimeUnit;
public class SlowApp {
public static void main(String[] args) throws InterruptedException {
System.out.println("Starting slow app...");
produceData();
System.out.println("Slow app finished.");
}
private static void produceData() throws InterruptedException {
for (int i = 0; i < 1000000; i++) {
processItem(i);
if (i % 100000 == 0) {
TimeUnit.MILLISECONDS.sleep(10); // Simulate some work/blocking
}
}
}
private static void processItem(int item) {
// Simulate some CPU-bound work
double result = 0;
for (int j = 0; j < 1000; j++) {
result += Math.sin(item * Math.PI / j);
}
// Suppress unused variable warning
System.nanoTime();
}
}
We can start this application and simultaneously enable Flight Recorder to capture detailed execution data. We’ll use jcmd to interact with the running JVM. First, find the PID of your SlowApp. Let’s assume it’s 12345.
To start recording, we’ll use jcmd with the JFR.start command:
jcmd 12345 JFR.start name=MyProfilingSession,filename=myrecording.jfr,settings=profile
This command tells the JVM with PID 12345 to start a Flight Recorder session named MyProfilingSession, save the output to myrecording.jfr, and use the profile settings, which are optimized for detailed CPU profiling. The profile settings include events like jdk.ExecutionSample, which periodically samples the call stack to identify hot methods.
Let the application run for a while, or until it completes. Once you’re done, stop the recording:
jcmd 12345 JFR.stop name=MyProfilingSession
This will save the collected data into myrecording.jfr. Now, you can analyze this file using tools like JDK Mission Control (JMC) or the jfr command-line tool. JMC provides a rich graphical interface.
When you open myrecording.jfr in JMC, you’ll see various views. The "Flame Graph" or "Method Profiling" views are excellent for identifying where your application spends its CPU time. You’ll likely see processItem and potentially the Math.sin and the inner loop as major contributors. The TimeUnit.MILLISECONDS.sleep(10) call might also appear if the sampling interval captures it, indicating periods of intentional blocking.
Flight Recorder works by enabling a set of event listeners within the JVM. When certain events occur (like method entry/exit, garbage collection, thread blocking, etc.), the JVM records them. For profiling, the key event is jdk.ExecutionSample, which takes a snapshot of the thread’s call stack at regular intervals. The profile settings configure these events and their sampling rates. The low overhead is achieved through a highly optimized, lock-free, circular buffer in memory where events are written. When the buffer is full, it’s written to disk, or if the recording stops, the remaining buffer content is flushed.
The settings=profile option is crucial. It’s a pre-configured set of events that are generally useful for performance analysis. You can also specify custom settings by referencing a .jfc file. For example, you might want to include more detailed garbage collection information or lock contention events.
Here’s a more advanced command to record for 60 seconds with custom settings:
jcmd 12345 JFR.start name=MyAdvancedProfiling,filename=myadvanced.jfr,duration=60s,settings=default+unlock.jfc
This starts a recording for 60 seconds, saving to myadvanced.jfr, using the default settings plus events defined in unlock.jfc (which you’d need to create or find, containing custom event configurations).
The real power comes from understanding the events. Flight Recorder can capture hundreds of different event types, from JVM internal operations to application-specific events you can emit yourself using the jdk.jfr.Event API. This allows for incredibly deep introspection without significant performance impact, often less than 1-2% overhead.
One thing many developers miss is the ability to correlate application behavior with JVM internals. For instance, you can see how much time is spent in garbage collection, how long threads are blocked waiting for locks, or even the exact allocation sites of objects that are being garbage collected frequently. This level of detail allows you to move beyond guessing about performance bottlenecks and pinpoint them with certainty. For example, if you see a lot of time spent in GC.ALLOC_ તે, it means your application is creating a lot of short-lived objects, which can put pressure on the garbage collector.
Once you’ve identified a bottleneck, say processItem is too slow, you’d then go back to your Java code and optimize that specific method, perhaps by algorithmic changes or by offloading work to other threads if it’s amenable to parallelization.
After fixing the performance issue and re-running your application with Flight Recorder enabled, you’ll likely encounter the next most common performance problem: excessive garbage collection.