必须先采集的5个基础运行时指标:jstat -gc PID 1000 5(看GCCapacity、GCCount、GCTime)、jstat -gcmetacapacity PID、jstack统计BLOCKED/WAITING线程数、top -H定位高CPU线程、jmap -histo:live PID前20行对象分布。

必须先采集的5个基础运行时指标
不记录这些数据就调JVM参数,等于蒙眼开车。重点不是“全量采集”,而是盯住真正能归因的几个数字。
-
jstat -gc PID 1000 5:每秒采样一次、共5次,看GCCapacity、GCCount、GCTime三列。其中GCTime直接用于算吞吐量(比如一小时里GC占了90秒,吞吐量就是(3600-90)/3600 ≈ 97.5%) -
jstat -gcmetacapacity PID:专看元空间是否持续增长。如果MCMN(最小元空间容量)和MC(当前容量)差值越拉越大,大概率是类加载泄漏,不是堆调优能解决的 -
jstack PID | grep 'java.lang.Thread.State' | wc -l:统计BLOCKED和WAITING线程总数。超过活跃线程数的10%,说明锁或资源竞争已成瓶颈,此时调堆大小意义不大 -
top -H -p PID后按P排序:找CPU占用最高的Java线程,用printf '%x\n' 线程ID转16进制,再在jstack输出里定位具体栈帧——很多“慢接口”实际卡在String.substring或HashMap.get这种看似安全的操作上 -
jmap -histo:live PID | head -20:只看前20行,重点关注char[]、byte[]、LinkedHashMap$Entry这类对象实例数是否异常高。比如char[]实例数突增10倍,基本可断定是日志拼接或JSON序列化没控制长度
GC日志里必须grep的3类关键词
开-Xlog:gc*:file=gc.log:time,level后,别通读日志,直接用命令筛关键模式。
-
grep 'Pause Young' gc.log | tail -10:查最近10次Young GC停顿时间。若频繁出现>50ms,说明新生代太小或对象晋升过快;若3次/秒,大概率是对象分配速率过高(比如循环里new HashMap) -
grep 'Full GC\|Pause Full' gc.log | tail -5:只要命中任意一行,立刻停止调优,先查内存泄漏。OpenJDK 11+默认G1,出现Full GC基本等于配置或代码有硬伤 -
grep 'Mixed' gc.log | awk '{print $9}' | sort -n | tail -1:提取G1 Mixed GC的停顿毫秒值并取最大值。超过200ms要警惕,这说明老年代碎片化严重,可能需要调-XX:G1HeapWastePercent或-XX:InitiatingOccupancyPercent
容器环境额外盯死的2个边界值
在K8s或Docker里跑Java,宿主机资源可见性被削弱,以下两个值不监控等于放弃底线。
-
cat /sys/fs/cgroup/memory/memory.limit_in_bytes:容器内存上限。如果这个值是2G,但你设了-Xmx3g,JVM会静默降级到2G,且jstat显示的堆大小仍是3G——这是假象,实际触发OOM Killer的是cgroup限制 -
cat /sys/fs/cgroup/cpu/cpu.cfs_quota_us除以cpu.cfs_period_us:算出CPU配额。比如结果是0.5,意味着Java进程最多用半个核。此时就算jstat显示GC时间占比才2%,但真实业务线程可能因抢不到CPU而排队,响应时间照样飙升
为什么不能只信VisualVM或Prometheus图表
它们聚合了采样数据,会平滑掉瞬时尖刺。一个典型的坑:某支付服务TP99突然从80ms跳到1200ms,监控图上看只是“小幅波动”。但用jstat -gc PID 100 10(100ms粒度采样10次)一抓,会发现第3次采样时GCTime暴涨了400ms——这对应一次长停顿GC,而聚合图表把它平均掉了。
真正要命的从来不是平均值,而是那些躲过监控、只在压测毛刺里露头的单点长停顿。记录原始指标时,宁可多存几份jstat快照,也别依赖仪表盘的“看起来很稳”。











