jstat -gcutil
The problem. At 3am you do not have a GC log configured and you need to know now whether the collector is eating the CPU. jstat prints the JVM's own GC counters live, once a second.
What you need to know already: GC kinds and pauses (20.12), the attach rules and hsperfdata (20.3), top -H to see threads (3.5).
A live view of the collector, one row per interval:
$ sudo -u appuser jstat -gcutil $(pgrep -f orders.jar) 1000 5
S0 S1 E O M CCS YGC YGCT FGC FGCT CGC CGCT GCT
0.00 42.72 82.52 32.03 98.70 98.11 6 0.033 0 0.000 0 0.000 0.033
0.00 42.72 92.29 32.03 98.70 98.11 6 0.033 0 0.000 0 0.000 0.033
0.00 42.72 2.05 32.48 98.70 98.11 7 0.039 0 0.000 0 0.000 0.039
0.00 42.72 11.83 32.48 98.70 98.11 7 0.039 0 0.000 0 0.000 0.039
0.00 42.72 21.60 32.48 98.70 98.11 7 0.039 0 0.000 0 0.000 0.039
1000 is the interval in ms (1s also works), 5 the number of samples; without a count it runs until Ctrl+C.
S0 S1 survivor spaces, % used (G1: S0 stays 0.00)
E eden, % used climbs, drops to ~0 at each young GC
O old generation, % used THE column: its floor after GCs
M metaspace, % of committed near 100% is normal - it is % of committed, not a limit
CCS compressed class space, % of committed
YGC young GC count since start YGCT total seconds in young GCs
FGC full GC count FGCT total seconds in full GCs
CGC concurrent cycle pauses (G1 remark/cleanup) CGCT their time
GCT all GC time, seconds
The third row shows a young GC: E went 92 -> 2, YGC 6 -> 7, O up by half a percent (promotion). Nothing to see - healthy.
Rates, not totals
The counters are cumulative since JVM start. What matters is how fast they move. Two samples 10 seconds apart:
YGC 802 -> 811, YGCT 3.310 -> 3.349 9 young GCs, 39 ms of pause in 10 s 0.4% of time
FGC 1 -> 1 no full GCs
A JVM thrashing looks like this instead:
S0 S1 E O M CCS YGC YGCT FGC FGCT CGC CGCT GCT
0.00 0.00 100.00 99.87 98.70 98.11 2310 12.410 418 187.211 52 0.140 199.761
0.00 0.00 100.00 99.91 98.70 98.11 2310 12.410 420 188.140 52 0.140 200.690
0.00 0.00 100.00 99.94 98.70 98.11 2310 12.410 422 189.071 52 0.140 201.621
Old at 99.9% and staying there, eden at 100% (it cannot be emptied: nowhere to promote), FGC going up by two every second and FGCT by almost a second per second. The JVM is spending ~93% of its time in Full GC. That is your 100% CPU.
jstat -gccause adds the reason for the last and current collection:
$ sudo -u appuser jstat -gccause $(pgrep -f orders.jar) 1s 2
... GCT LGCC GCC
... 201.621 G1 Compaction Pause G1 Compaction Pause
The decision procedure for "Java is at 100% CPU"
- Is it the GC?
jstat -gcutil PID 1s 10. If FGC/FGCT climb every second and O sits near 100: it is the collector. Heap too small for the live set, or a leak. Go to the histogram (later lesson). - Is it one thread?
top -H -p PIDshows threads by CPU. Convert the hot thread id to hex (printf '%x\n' 1297) and findnid=...in a thread dump - modern dumps print the decimal nid too, which saves the conversion. - Is it all the request threads? A thread dump with many
RUNNABLEthreads in your own code: genuine load, or an inefficient loop. A profiler answers where:jcmd PID JFR.start duration=60s filename=/tmp/cpu.jfrand open it in JDK Mission Control, or async-profiler for a flame graph. - Is it the JIT?
C2 CompilerThread0busy right after startup is warm-up and settles in a minute or two.
jstat -gc: capacities in KB
$ sudo -u appuser jstat -gc $(pgrep -f orders.jar)
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT CGC CGCT GCT
0.0 8192.0 0.0 3500.0 62464.0 20480.0 301056.0 150112.0 99602.0 98304.0 13568.0 13312.0 7 0.039 0 0.000 0 0.000 0.039
C = capacity, U = used, all in KB. OU over time is the old-gen floor in absolute numbers.
What jstat cannot tell you
- What is in the old generation - that needs a histogram or a heap dump.
- Anything about native memory - use NMT.
- Anything if you are the wrong user:
1210 not foundmeans "I cannot read its hsperfdata", not "no such process".