1. Overview
Profilers expose a large volume of runtime data in the form of graphs, numbers, and timelines. Without a clear set of metrics, that output is difficult to interpret and act on.
This lesson introduces the essential profiling metrics across four key groups: CPU, memory, threads, and external interactions. Understanding these categories helps us interpret profiler output and recognize the key indicators of performance issues.
There is no code we need to check out to follow along with this lesson.
2. CPU Metrics
CPU metrics show where the application spends processor time and which code paths are most expensive.
High CPU usage is not inherently bad, but we need to make sure our code is efficiently using the available processing power.
2.1. Execution Time
Execution time is the total duration it takes for an operation to complete. Profilers typically report this in two distinct ways:
- Wall Clock Time: This represents the total elapsed time from when a method starts to when it finishes. In addition to the method’s business logic, it also includes all waiting periods, such as time spent executing database queries, network calls, and I/O operations.
- CPU Time: This measures only the time the processor actively spends executing our code and excludes any periods when the thread is waiting or blocked.
This distinction helps us categorize the problem. If a method has high wall clock time but low CPU time, it’s likely waiting on an external resource. Conversely, if both metrics are high, the method itself contains expensive computations that might need optimization.
2.2. Method Hotspots
In most applications, the 80:20 rule applies, meaning that roughly 80% of the execution time is spent executing just 20% of the code. We call these resource-intensive areas “hotspots”.
Identifying hotspots is a direct way to improve CPU efficiency. Instead of trying to optimize every single method, we focus our efforts on the top methods consuming the most CPU time. A small improvement in a major hotspot can yield significant overall gains.
Profiler views usually show two dimensions for each method: total time and invocation count. Considering both helps distinguish expensive work from frequent work. The data collection method affects how accurate those dimensions are. While sampling is ideal as an initial step to identify general CPU hotspots, it often lacks the precision to provide exact invocation counts. If we need precise counts for a specific method, we need to enable instrumentation, which has a higher performance cost.
A method might appear as a hotspot because it is called millions of times, even if each individual call is fast. In such cases, reducing the number of calls or caching results can be more effective than micro-optimizing the method itself.
3. Memory Metrics
Memory metrics help us ensure that our application uses RAM efficiently. They reveal how our application allocates, uses, and releases memory within the JVM.
3.1. Heap and Non-Heap Memory
The heap is the runtime memory area where all Java objects live. Monitoring heap usage allows us to visualize how much memory our application consumes over time. In a healthy application, we typically see a sawtooth pattern where memory usage rises as objects are created and drops sharply when garbage collection occurs.
If we observe a graph with a steadily rising baseline, where each garbage collection cycle reclaims less memory than before, we are likely facing a memory leak, eventually leading to an OutOfMemoryError.
Profilers can also track non-heap memory. Leaks in this area are rarer but can still crash an application.
3.2. Allocation and Retention
Beyond where memory lives, profilers also show how quickly objects are allocated and how long they are retained. When analyzing these signals, we must distinguish between allocation and retention issues, as they require different fixes:
- Allocation Rate: This measures how many megabytes of objects are created per second. A high allocation rate puts pressure on the garbage collector, especially when our application rapidly creates and discards short-lived objects such as temporary strings or collections. The fix here is often to reduce unnecessary object creation and reuse existing objects.
- Retained Memory: This measures objects that survive garbage collection and remain in the heap. If such long-lived objects continuously fill the heap with no business justification, it often points to a memory leak.
Understanding these metrics in the heap allows us to pinpoint whether we’re dealing with a high allocation rate, a memory leak, or both.
3.3. Garbage Collection Activity
The garbage collector (GC) automatically reclaims memory from objects that are no longer referenced. While this frees us from manual memory management, garbage collection consumes CPU cycles that could otherwise execute our application code.
Profilers track garbage collection frequency, duration, and the amount of memory reclaimed in each cycle. Frequent collections are usually a symptom of underlying allocation or retention issues, rather than the root cause itself.
During certain collection phases, the JVM triggers stop-the-world pauses that halt all application threads. If our application freezes for seconds at a time, we must investigate what is filling the memory so fast, whether it’s a high allocation rate or a memory leak reducing the available free space.
4. Thread Metrics
Thread metrics reveal how execution time is distributed across threads and whether concurrency is helping or hurting throughput. They allow us to visualize the concurrency model of our application and ensure that our threads are working together efficiently rather than contending for resources.
4.1. Thread States
The JVM defines several states a thread can exist in at a given point in time. Profilers help visualize these states over time, allowing us to see what our threads are actually doing.
A thread in the RUNNABLE state is actively executing or ready to execute when a processor becomes available. This is the healthy, productive state where threads perform actual work.
Meanwhile, threads in BLOCKED, WAITING, or TIMED_WAITING states are not actively processing. A BLOCKED thread is waiting to acquire a lock held by another thread, while WAITING and TIMED_WAITING threads are either idle, waiting for a signal, or sleeping.
By analyzing the distribution of these states, we can detect if our application is suffering from concurrency issues.
4.2. Contention and Locking
When multiple threads attempt to acquire the same lock simultaneously, only one succeeds while others enter the BLOCKED state. This situation is called lock contention, and the time threads spend waiting to acquire locks is contention time.
Profilers measure contention by tracking how often threads block on specific locks and for how long. High contention on a single lock indicates that a shared resource has become a bottleneck, forcing threads to queue up instead of executing in parallel.
In some cases, this can even cause deadlocks, where two or more threads wait on each other indefinitely. Profilers also help us detect this by identifying threads that wait on each other without making progress.
5. Interaction Metrics
Modern distributed applications rarely exist in isolation and frequently interact with external systems. These interactions often involve network and I/O latency, which can dominate end-to-end response time. This category includes databases, outbound HTTP calls, messaging systems, file I/O, and other external services.
5.1. Database Queries
For many data-driven applications, database interactions are often the slowest part of a request. Profiling JDBC activity allows us to see exactly which SQL queries are being executed, how often they run, and how long they take.
A query might be slow or frequent, and profilers help us see cumulative time, execution counts, and the most expensive statements. We also look for red flags such as the N+1 select problem, where a single logical operation triggers hundreds of individual database calls.
Additionally, we monitor the database connection pool to ensure that threads are not waiting excessively just to obtain a connection to the database.
5.2. Web Request Latency
In a web application, the most visible metric to the end user is request latency, which is the total time taken between the server receiving an HTTP request and sending the response.
One key metric is throughput, which is measured in Requests Per Second (RPS). A healthy application should be able to maintain stable throughput as traffic increases, up to its capacity limit.
However, averages can be misleading. In addition to throughput, profilers also capture the response time distribution using percentiles. For instance, the 95th percentile (P95) and 99th percentile (P99) tell us how the slowest 5% and 1% of requests are performing, respectively, revealing issues that averages may otherwise hide.
6. Conclusion
In this lesson, we’ve explored the key metrics that profilers capture to help us understand our application’s performance.
We started with CPU metrics, learning how execution time and hotspots reveal where our code spends the most processing power. Then, we examined memory metrics to understand heap usage and garbage collection costs.
Next, we covered thread metrics to observe concurrency behavior and detect contention issues. Finally, we looked at how interaction metrics highlight bottlenecks when communicating with external systems.
With a solid understanding of these metrics, we’re now equipped to interpret profiler output and make data-driven decisions to optimize our application’s performance.