Skip to content

JVM Diagnostic Tools: jmap, jstack, jstat & Arthas

It is 2 AM. The on-call phone rings: one service is throwing OutOfMemoryError, another responds every 30 seconds, and a third burns 100% CPU. Every JDK ships with the tools you need for all three — this guide walks through them by scenario, not by alphabet.

The toolbox at a glance

ToolWhat it doesTypical moment
jpslist Java process PIDsalways the first command
jmapmemory histogram, heap dumpOOM, memory leaks
jstackthread dump, deadlock detectionstuck requests, hangs
jstatGC statistics samplingwatching GC behavior live
jcmdSwiss-army knife (JDK 8+)everything above, one entry point
JMX + VisualVMremote visual monitoringlong-running observation
Arthasinteractive live diagnosiscannot restart, need answers now

Scenario 1: the service died with OutOfMemoryError

Goal: find which objects ate the heap.

The most reliable evidence is a heap dump captured at the moment of death. Plant the trap in advance:

bash
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/dump/

When the OOM strikes, the JVM writes a .hprof file automatically. Open it in Eclipse MAT or VisualVM and look at the dominator tree: one or two objects usually hold most of the heap. Classic culprits from real incidents:

  • an unbounded HashMap used as a cache with no eviction policy,
  • a thread-local list that never gets cleaned in a thread pool,
  • a query result loaded into memory without pagination (a "small" report query over 8 million rows).

Need a quick answer without downloading the dump? The histogram lists instances and bytes per class:

bash
jmap -histo <pid> | head -30        # all objects
jmap -histo:live <pid> | head -30   # live only — triggers a Full GC, use off-peak

A business class appearing thousands of times in the top rows usually points straight at the bug.

Scenario 2: requests hang, the service is frozen

Goal: find out what the threads are waiting for.

bash
jstack <pid> > thread-dump.txt

Search the dump for BLOCKED states and for the line Found one Java-level deadlock — the JVM detects classic lock cycles for you and prints the two locks involved. The typical root cause is inconsistent lock ordering: method A locks resource 1 then 2, method B locks 2 then 1. The fix is to always acquire locks in the same global order — or better, replace hand-rolled synchronization with java.util.concurrent primitives.

Take three dumps a few seconds apart: a thread that stays BLOCKED on the same lock across all three is your stuck request; a thread whose stack changes is just slow, not stuck.

Scenario 3: CPU at 100%

Goal: find the one thread burning a core.

bash
top -Hp <pid>              # list threads of the JVM, note the top thread id
printf '%x\n' <tid>        # convert to hex, e.g. 0x2d64
# then search the jstack dump for nid=0x2d64

Two common outcomes:

  • the thread's stack shows your code in a loop — a real hot loop bug;
  • the top consumers are GC threads — the "CPU problem" is actually a GC problem. Confirm with jstat -gcutil <pid> 1000: if FGC keeps climbing, continue with a GC log analysis.

Scenario 4: watch GC behavior live with jstat

jstat samples GC counters without stopping anything:

bash
jstat -gcutil <pid> 1000 10   # every 1s, 10 samples, percentages per generation
jstat -gc <pid> 1000 10       # absolute KB values

-gcutil shows Eden / Survivor / Old / Metaspace occupancy as percentages plus YGC, FGC and their accumulated times. From a short series you can estimate:

  • young-gen fill rate — how fast Eden fills between samples → predicts Young GC frequency;
  • average Young GC timeYGCT / YGC;
  • promotion pressure — how much the Old gen grows after each Young GC;
  • Full GC costFGCT / FGC.

That estimation workflow is exactly what the EasyGC report automates from a full GC log — with charts and leak detection on top.

Scenario 5: watch a remote JVM visually

For long-running observation, connect VisualVM or another JMX client over the network. The target JVM needs:

bash
-Dcom.sun.management.jmxremote.port=8888
-Djava.rmi.server.hostname=<server-ip>
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false

WARNING

authenticate=false exposes the JVM to anyone who can reach the port. Bind to an internal interface, use a firewall, and never expose JMX to the public internet.

The Visual VM plug-in "Visual GC" renders the generations in real time — a great way to feel the allocation pattern of an application during a load test.

Scenario 6: you cannot restart, you need answers now

Arthas (Alibaba, open source) attaches to a running JVM and offers an interactive shell: dashboard for a live overview, thread -b to locate deadlocks directly, jad to decompile a class and check which version actually runs, profiler for flame graphs of hot paths. When a production issue cannot wait for a redeployment, it is the fastest path from symptom to root cause.

A practical triage order

  1. jps → find the PID.
  2. jstat -gcutil → is it a GC problem? (if yes → GC log analysis)
  3. jstack ×3 → is it a lock/latency problem?
  4. jmap -histo or heap dump → is it a memory problem?
  5. Arthas / JMX → live follow-up questions.