Profiling Java with JDK Flight Recorder, explained simply
You've deployed a service that's slowing down under load, but you're not sure where the CPU time is actually going. Your team is torn between adding more heap or optimizing code that might not even be the bottleneck. This is where profiling Java with JDK Flight Recorder becomes essential—it's a low-overhead production profiler built directly into the JDK that lets you capture exactly what your application is doing without redeploying or restarting.
JDK Flight Recorder (JFR) has been available since Java 11 and is the gold standard for production profiling. Unlike traditional profilers that can impose 10-20% overhead, JFR typically costs only 1-2% and can be toggled on and off at runtime. We'll walk through capturing recordings, analyzing them in JDK Mission Control, and understanding hot methods, memory allocations, and lock contention—all before you write a single optimization.
Starting a recording with jcmd
The simplest way to begin profiling is through jcmd, a command-line tool that ships with every JDK installation. No agents, no flags at startup, no restarts needed. First, find your application's process ID:
jcmd -lThis lists all running Java processes. Let's say your Spring Boot application has PID 12345. Start a JFR recording with a single command:
jcmd 12345 JFR.start name=MyRecording duration=60s filename=/tmp/myrecording.jfrThis captures 60 seconds of runtime data and writes it to a file. You can also start indefinitely and stop manually:
jcmd 12345 JFR.start name=MyRecording
jcmd 12345 JFR.stop name=MyRecording filename=/tmp/myrecording.jfrThe recording file is compact—typically 10-50 MB per minute depending on event density. Real-world scenario: your e-commerce checkout service is handling peak traffic. You fire up a JFR recording, let it run through a few hundred transactions, then stop it. The entire overhead is negligible compared to the insight you'll gain.
In a Kubernetes environment, you might exec into a pod, run jcmd, then copy the file out. For Docker containers, ensure the JDK is included in your image (not just a JRE) and that you have shell access to run these commands.
Opening and navigating JDK Mission Control
JDK Mission Control (JMC) is the GUI for analyzing JFR recordings. Download it from jdk.java.net/jmc or use it if your IDE has built-in support (IntelliJ IDEA ships it).
Open your recording file in JMC, and you'll see a timeline view of your application's behavior. The interface shows tabs for different event categories: CPU, Memory, Threads, I/O, and more. Each tab breaks down what happened during your recording window.
Start with the Automated Analysis tab. JMC scans your recording for common issues—high memory allocation rates, excessive GC pauses, lock contention, thread issues—and flags them with severity levels. This gives you immediate guidance on where to look first.
The Method Profiling view shows CPU usage by method, ranked by total time spent. Memory Allocation shows which object types are being created most frequently and by which call stacks. Thread View reveals lock contention and thread parking patterns. All of these are sortable and filterable.
Identifying hot methods and CPU hotspots
Hot methods are functions consuming the most CPU time. In JMC, navigate to Method Profiling and sort by Total Time. You'll see a tree of call stacks, each with timing information.
Real example: you're analyzing a Spring Data query service. The recording might show that 40% of CPU time is in org.springframework.data.jpa.repository.support.SimpleJpaRepository.findAll() because it's fetching 100,000 rows without pagination. The next highest might be time spent in object deserialization in your JSON mapper.
The key insight is always profile before optimizing. You might intuitively think you need to cache results, but the recording tells you the real story: the database round-trip is fast, but serialization is slow. Now you optimize with certainty.
JMC shows you not just the hot methods, but their call chains. Click a method to see who calls it and what it calls. This context is crucial for understanding whether a high time is legitimate or a sign of inefficiency.
Memory allocations and garbage collection insights
Memory allocation tracking is one of JFR's superpowers. Switch to the Memory tab and view Allocation by Class. You'll see which object types are being created most frequently during your recording.
In a typical web request, you might see millions of small String allocations, byte arrays from JSON parsing, or HashMap instances from Spring's context handling. JFR tells you not just what is allocated, but where—the call stack that created it.
Here's a scenario from a real SaaS application: the memory view showed that 30% of allocations came from java.util.HashMap.put called from a logging library. Investigation revealed that log messages were being formatted with expensive operations on every single request, even at INFO level. Fixing the log template reduced allocations by 20% and CPU by 8%.
The Garbage Collection section shows pause times, collection frequency, and heap pressure. If you see frequent full GCs or long pause times, JFR's memory allocation data tells you which object types to target for reduction or pooling.
Key point: allocations don't just cost memory—they cost CPU during allocation, copying during GC, and cache coherency. Reducing allocations often reduces CPU usage even if heap isn't the constraint.
Lock contention and thread analysis
In multithreaded applications, contention on locks can silently destroy throughput. JMC's Threads tab shows lock events: which locks are contested, how often threads wait on them, and for how long.
Real scenario: a trading platform's order processing service has synchronized blocks protecting shared order books. JFR reveals that 15% of CPU time is spent waiting for a single lock. The recording shows thread IDs, exact line numbers, and wait durations. Armed with this, the team decides to use concurrent data structures or fine-grained locking to reduce contention.
JMC visualizes thread state transitions: RUNNABLE, WAITING, PARKED, BLOCKED. If a critical thread spends 30% of time PARKED waiting on a ReentrantLock, that's a red flag. If it's WAITING on a network call, that's normal.
The Hotlocks view ranks locks by total wait time. Look for surprising entries—libraries you didn't expect to be contended, or application-level locks that should have been replaced with concurrent structures long ago.
Common mistakes when profiling
Running too short a recording. A 5-second recording might miss infrequent but expensive operations. For request-driven services, aim for at least 30-60 seconds at realistic load. For batch jobs, run until completion plus a bit of idle time.
Profiling a completely quiet system. JFR data is most meaningful under realistic load. Start a recording, then trigger the behavior you want to understand: run your load test, execute a batch job, process a backlog. Profiling idle time wastes the insight.
Ignoring the automated analysis. JMC's built-in checks catch 80% of common issues. If you jump straight to raw method timings, you'll miss structural problems like excessive thread creation, huge allocation spikes, or GC pauses that are easier to spot in the analysis view.
Over-interpreting tiny percentages. If a method shows 0.3% of CPU time, it's rarely worth optimizing. Focus on the top 5-10 contributors. There are always diminishing returns.
Forgetting to enable JFR in production. By default, JFR is enabled in most JDK distributions, but some minimal images disable it. Test that jcmd works in your production environment before you need it in a crisis.
Key takeaways
- Start profiling with
jcmd <pid> JFR.start—no restart required, minimal overhead. - Open recordings in JDK Mission Control and begin with the Automated Analysis tab for quick insights.
- Always examine hot methods first, then memory allocations, then lock contention.
- Profile under realistic load with sufficient duration to capture the behavior you care about.
- Never optimize based on intuition alone—let JFR data guide your priorities.
- Use JFR regularly, not just in crises; it becomes your team's shared language for performance discussions.






Comments
Loading comments…