Definition
Profiling means measuring where a running application spends its time and memory so you can find bottlenecks with data instead of guesswork. Java Flight Recorder (JFR) is a built-in, very low-overhead recorder that captures detailed runtime events and is safe to run in production. Java Mission Control (JMC) is the desktop tool that opens and analyzes JFR recordings. VisualVM is a free tool for live heap, thread, and CPU inspection. async-profiler is a sampling profiler that produces accurate CPU and allocation flame graphs with minimal overhead.
Understanding the Concepts
What is profiling?
Profiling means measuring where a running application actually spends its time and memory so you can find bottlenecks with data instead of guesswork. The first rule is to never optimize based on a guess – measure first, optimize second.
Java Flight Recorder (JFR)
JFR is built into the JDK and records a stream of events – method execution samples, allocations, GC pauses, lock contention, thread states, I/O – with overhead usually around one percent. This low overhead makes it safe to run in production.
# Start a recording at launch
java -XX:StartFlightRecording=duration=60s,filename=recording.jfr -jar bank-api.jar
# Attach to a running process
jcmd <pid> JFR.start duration=60s filename=/tmp/bank-recording.jfr
# Stop a recording
jcmd <pid> JFR.stop name=1
The result is a .jfr file you analyze later. The key strength of JFR is that it is always-affordable, so you can capture the exact moment a problem happens in production rather than trying to reproduce it in a lab.
Java Mission Control (JMC)
JMC is the desktop analysis tool that opens .jfr files. It gives you ready-made views:
- Hot methods: where CPU time went, ranked by self-time.
- Allocation view: which code allocates the most objects and bytes.
- GC view: pause times, frequencies, and heap usage over time.
- Lock contention: which monitors have the most waiting threads.
- Automated analysis: flags likely problems with explanations.
package com.bank.service;
import java.math.BigDecimal;
// JFR might reveal that this method is the top CPU consumer
// during the nightly interest batch because of BigDecimal division
public class InterestCalculator {
public BigDecimal calculateMonthly(BigDecimal balance, BigDecimal annualRate) {
return balance.multiply(annualRate)
.divide(new BigDecimal("1200"), 10, java.math.RoundingMode.HALF_UP);
}
}
VisualVM
VisualVM is a free, lightweight tool for live inspection. You attach it to a running JVM and watch heap usage, thread states, and a sampling CPU profiler in real time. It is excellent for quick, interactive investigation on a development or staging machine, and it can take and compare heap dumps to help find memory issues.
async-profiler and flame graphs
async-profiler is a low-overhead sampling profiler especially good at CPU and allocation profiling. It produces flame graphs, where the width of each box shows how much time (or allocation) a method and its callees consumed, so the widest boxes are your biggest costs.
# CPU flame graph for 30 seconds
./asprof -d 30 -f cpu-flame.html <pid>
# Allocation flame graph
./asprof -d 30 -e alloc -f alloc-flame.html <pid>
Because async-profiler samples using operating-system and JVM mechanisms, it avoids the "safepoint bias" that can mislead simpler profilers. Safepoint bias means a profiler that only samples at JVM safepoints misses hot code between safepoints, giving a distorted picture.
What are common profiling mistakes?
- Profiling in development instead of production-like conditions. Development machines have different hardware, data volumes, and concurrency. Results do not transfer.
- Profiling without JIT warm-up. The first few thousand calls are interpreted and show misleading hot spots.
- Optimizing the wrong dimension. CPU time for compute bottlenecks, allocation rate for GC pressure, wall-clock time for latency including waiting.
- Changing multiple things at once. Change one thing, re-profile, and confirm it actually helped before moving on.
In banking at National Bank of India, Rajesh always profiles under realistic load after warm-up. When the statement generation batch became slow, a JFR recording showed that 80% of CPU was spent in string concatenation inside a loop, which was also the top allocator driving GC pressure. The fix was to replace concatenation with StringBuilder.
Key Takeaway
Profiling means finding where an application actually spends its time and memory using measurement, not intuition, and the first rule I follow is to never optimize based on a guess. The tool I reach for first is Java Flight Recorder, which is built into the JDK and records a rich stream of events – method samples, allocations, GC pauses, lock contention – with overhead of roughly one percent. That low overhead is the whole point, because it means I can leave it running in production and capture a problem the moment it happens, rather than failing to reproduce it in a test environment. I start it at launch with StartFlightRecording or attach to a running process with jcmd, and I get a dot-jfr file.
To analyze that file I use Java Mission Control, which opens the recording and gives me ready-made views: the hottest methods by CPU, which code allocates the most, GC behavior, and lock contention, plus an automated analysis that flags likely issues. For quick live triage on a development or staging machine I use VisualVM, which attaches to a running JVM and shows heap, threads, and a sampling CPU profiler in real time. And when I want precise CPU or allocation flame graphs, I use async-profiler, where the widest boxes in the flame graph are immediately my biggest costs, and it avoids the safepoint bias that can mislead simpler profilers.
In my banking work at National Bank of India, this exact toolkit resolved a real argument. Our end-of-day statement generation had become slow, and the team was split on whether the database or the Java code was at fault. I captured a JFR recording in production, opened it in JMC, and the hot-methods view showed that over eighty percent of the CPU was being spent building statement strings by concatenation inside a loop, which was also the top allocator and the source of our GC pressure. The database was innocent. I replaced the concatenation with a StringBuilder, re-profiled under the same load to confirm CPU and allocation both dropped, and only then shipped it. The discipline that made it work was profiling under realistic load after warm-up, measuring the right dimension, and changing one thing at a time while re-profiling to prove the fix.
Remember one sentence: How to profile a Java application: JFR (Java Flight Recorder) for low-overhead production profiling, JMC (Java Mission Control) for analysis, VisualVM for heap/thread inspection, async-profiler for CPU/allocation profiling is fundamental knowledge for Java interviews and production banking work.
---