JVM Monitoring: Diagnose with jcmd, jstat, jmap
The JVM’s garbage collector is often the silent performance killer, and understanding its behavior is key to unlocking performance, not just tweaking application code.
Let’s see how these tools give us a live look. Imagine we have a simple Java app running, maybe a web server.
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class MemoryHog {
public static void main(String[] args) throws InterruptedException {
List<byte[]> memory = new ArrayList<>();
Random random = new Random();
while (true) {
// Allocate about 1MB of memory
byte[] data = new byte[1024 * 1024];
random.nextBytes(data);
memory.add(data);
// Simulate some work and potential pauses
Thread.sleep(10);
if (memory.size() > 1000) {
// Periodically clear some memory to simulate application behavior
memory.subList(0, 500).clear();
}
}
}
}
First, we need the Process ID (PID) of our Java application. You can get this using jps:
jps -l
This will list running Java processes and their main class names. Find the PID corresponding to your MemoryHog application. Let’s assume the PID is 12345.
Now, let’s explore jcmd. This is your Swiss Army knife for JVM diagnostics. You can list available commands for a given PID:
jcmd 12345 help
This will show a long list of available jcmd commands. For monitoring, the most useful ones are GC.heap_info, GC.run, Thread.print, and VM.native_memory.
To get a quick overview of the heap:
jcmd 12345 GC.heap_info
This command provides details about the heap’s generation sizes (Young, Old, PermGen/Metaspace), used and committed memory, and GC activity. It’s a great starting point to see if your heap is filling up.
Next, jstat. This is your real-time monitoring tool, especially for GC. The -gc option is your best friend:
jstat -gc 12345 1000
This command will print GC statistics every 1000 milliseconds (1 second). You’ll see columns like S0C, S1C (Survivor space capacities), S0U, S1U (Survivor space used), EC (Eden capacity), EU (Eden used), OC (Old gen capacity), OU (Old gen used), MC (Metaspace capacity), MU (Metaspace used), YGC (number of Young GC events), YGCT (Young GC time), FGC (number of Full GC events), and FGCT (Full GC time). Observing YGC and FGC counts, along with their times, is crucial. If YGCT or FGCT are rapidly increasing, or if OU is consistently high, you’re likely experiencing memory pressure.
For a more detailed look at GC, jstat -gcutil 12345 1000 shows percentages of space used, which can be more intuitive.
jmap is powerful for creating heap dumps, but it also has a useful monitoring capability:
jmap -heap 12345
This command provides a detailed report on the heap, including the type of garbage collector being used, heap generation sizes, and a summary of objects in each generation. It’s more static than jstat but offers a deeper dive into heap configuration and object distribution. If you see excessive fragmentation or large object counts in the Old Generation, jmap -heap can highlight that.
To understand what’s consuming memory, you can generate a heap dump using jmap and then analyze it with tools like Eclipse Memory Analyzer (MAT) or VisualVM:
jmap -dump:format=b,file=heapdump.hprof 12345
This creates a binary heap dump file (heapdump.hprof) that you can load into a memory analyzer. Analyzing this dump can reveal the specific objects holding onto memory, helping you pinpoint memory leaks.
The JVM’s garbage collection policy is determined by the heap configuration and the chosen GC algorithm. For instance, if you see frequent Full GCs (FGC in jstat) and high FGCT, it indicates the JVM is struggling to reclaim memory, often due to objects accumulating in the Old Generation faster than they can be cleared. This could stem from an inefficient GC algorithm for your workload, or simply too small a heap for the application’s demands.
A common misconception is that jcmd GC.run will magically fix memory issues. While it forces a garbage collection cycle, it’s a diagnostic tool, not a performance tuning knob. If you’re constantly having to run GC.run to keep your application responsive, it’s a strong signal that your heap is too small or your GC algorithm is misconfigured for the application’s allocation patterns.
The GC pause times, visible in jstat’s YGCT and FGCT, are directly tied to application responsiveness. Long pauses, especially from Full GCs, freeze your application. When using a concurrent garbage collector like G1 or Parallel GC, understanding the MC (Metaspace Capacity) and MU (Metaspace Used) is also vital, as excessive Metaspace growth can also trigger GCs and, in extreme cases, OOM errors.
If you’ve tuned your heap size and GC parameters and still experience high FGCT and OU in jstat -gc, the next step is often to examine the object allocation patterns. Tools like jmap -histo:live 12345 can show you the number of instances and total size of live objects in the heap, sorted by class. This helps identify which classes are allocating the most objects.
After fixing memory leaks and tuning your heap, you’ll likely encounter issues related to thread contention or deadlocks.