Skip to Content
Course content

172: Profiling with JFR and VisualVM

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this happen dozens of times: a developer comes to me saying, "My app is slow, and I can see in Task Manager that Java is eating 90% of my CPU, so I just need to optimize my loops."

Stop Guessing with the OS Activity Monitor

The problem with relying on your OS's activity monitor is that it tells you that the JVM is working hard, but it tells you absolutely nothing about why. Is your business logic inefficient? Is the Garbage Collector (GC) thrashing because you've run out of heap space? Or are you hitting a synchronized block that's causing a massive thread bottleneck? When you see 90% CPU usage, you're looking at the symptom, not the disease. If you start optimizing loops blindly, you're just guessing, and in a complex system, guessing is a great way to waste three days of your life.

To actually fix performance, you need to move from "outside-in" monitoring (the OS) to "inside-out" profiling (the JVM). This is where Java Flight Recorder (JFR) and VisualVM come in.

Letting JFR Record the Crime Scene

Think of JFR as the "black box" flight recorder on an airplane. It's built directly into the JVM, which means it has incredibly low overhead—usually less than 1%. I often leave it running in production environments because it allows me to diagnose a spike that happened at 3:00 AM without having to try and "reproduce" it locally (which is usually impossible).

JFR doesn't just sample the CPU; it records specific events. For example, if you're dealing with a service that processes large XML files and suddenly slows down, JFR can tell you exactly how many TLAB (Thread Local Allocation Buffer) allocations are happening and which methods are triggering the most GC pressure.

# Start your app with JFR enabled
java -XX:StartFlightRecording=duration=60s,filename=my_recording.jfr -jar app.jar

In that one command, I've told the JVM to record everything for 60 seconds and dump it into a file. I don't have to change a single line of code. You can then open this .jfr file in JDK Mission Control (JMC) to see a timeline of exactly what happened.

Using VisualVM to Perform the Autopsy

While JFR is great for "what happened in the past," VisualVM is my go-to for "what is happening right now." It's a visual tool that connects to a running JVM via JMX (Java Management Extensions).

Imagine you have a caching layer in your app that's supposed to keep the last 1,000 users in memory, but your memory usage is climbing linearly. In VisualVM, you can hit the "Heap Dump" button. This freezes the JVM for a second and takes a snapshot of every single object on the heap. I can then sort by "Retained Size" and instantly see that a HashMap in my UserCache class is actually holding 500,000 entries because someone forgot to implement the eviction logic.

You can also use the "Sampler" tab. Unlike JFR, which is a recording, the Sampler lets you watch the CPU hotspots in real-time. If you see a method like java.util.regex.Pattern.match taking up 40% of your CPU, you know exactly where to look: you're probably compiling a regex inside a loop instead of reusing a static Pattern object.

  • Use JFR when: You need production-grade data, you're hunting a transient bug, or you can't attach a debugger to the process.
  • Use VisualVM when: You're in development, you suspect a memory leak, or you want to see the immediate impact of a code change on heap usage.



📋 Practical Task

Exercise: Hunting the Memory Leak in the CSV-to-DB Pipeline

You have been given a simplified version of a data ingestion service. The service reads "customer records" from a simulated CSV stream and processes them. However, the application crashes with an OutOfMemoryError after processing about 100,000 records, even though the heap size is plenty for the actual data being processed.

Your Task:

  1. Run the provided IngestionService.java.
  2. Launch VisualVM and connect to the running IngestionService process.
  3. Use the Sampler or Monitor tab to observe the heap growth.
  4. Take a Heap Dump once memory usage exceeds 200MB.
  5. Analyze the heap dump to find the specific collection (List, Map, or Set) that is growing unbounded.
  6. Identify the line of code causing the leak (Hint: Look for a static collection used for "logging" or "tracking" that is never cleared) and fix it so the memory usage stabilizes.

// IngestionService.java (Snippet)
public class IngestionService {
    private static final List<String> processedIds = new ArrayList<>();

    public void processRecord(String record) {
        String id = record.split(",")[0];
        // Business logic here...
        doWork(record);
        
        // The leak is hidden here
        processedIds.add(id); 
    }
    
    private void doWork(String r) { /* simulated work */ }
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.