jvm性能监控需上线前建立可观测性,核心是gc日志全量开启、oom自动快照、容器资源感知和类加载追踪,并通过gc趋势、线程状态、系统负载三步法快速响应问题。

JVM性能监控不是等出问题再补救,而是从上线前就建立可观测性,把关键指标变成日常习惯。重点不在工具堆砌,而在指标选择、阈值设定和响应路径是否闭环。
上线前必须埋的监控点
没有预埋,事后分析就是盲人摸象。这几项参数建议统一纳入基础镜像或启动脚本:
- GC日志全量开启:-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/logs/gc-%p.log,确保每次GC都有时间戳、区域使用量、停顿毫秒级记录;
- OOM自动快照:-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/logs/heap.hprof,避免手动触发时进程已退出;
- 容器资源感知:-XX:+UseContainerSupport(JDK8u191+默认启用),让JVM正确读取Docker内存限制,避免堆大小超配引发OOMKiller误杀;
- 类加载追踪(可选):-XX:+TraceClassLoading,排查动态代理、热部署导致的元空间泄漏。
线上问题第一响应三步法
收到告警后,不急着重启,按顺序查这三项,80%的问题能快速收敛:
-
看GC趋势:用jstat -gcutil
2000持续观察,重点关注E区使用率是否长期>90%(新生代撑不住)、O区是否缓慢爬升(对象晋升过多或内存泄漏)、FGC是否突增(老年代压力大); -
看线程状态:jstack -l
| grep "java.lang.Thread.State" 统计各状态线程数,RUNNABLE过多说明CPU瓶颈,BLOCKED多说明锁竞争,WAITING/TIMED_WAITING集中某类操作(如数据库连接池耗尽、远程调用未响应); -
看系统负载:用pidstat -u -p
1 或 top -p ,确认是JVM自身问题还是宿主机资源争抢(如CPU steal高说明虚拟机被调度器抢占)。
典型故障对应动作清单
不同现象对应明确操作,避免无效排查:
- CPU持续100%:jstack抓栈 → 找到CPU占用最高的线程ID(十进制转十六进制)→ 在jstack输出中定位该线程栈 → 看是否在死循环、正则回溯、加解密等CPU密集操作;
-
内存缓慢上涨不释放:jmap -dump:format=b,file=heap.hprof
→ 用MAT打开,按“Leak Suspects”报告优先看,重点关注HashMap、ArrayList、ThreadLocal持有的对象; - 频繁Full GC但堆没满:检查是否发生“promotion failure”,即Survivor区放不下晋升对象,直接进老年代;调整-XX:SurvivorRatio或-XX:MaxTenuringThreshold;
- 线程数暴涨卡死:jstack统计线程总数,对比应用配置的线程池maxSize;检查是否有未关闭的Stream、未回收的Connection、或递归创建线程的逻辑。
调优落地要分层验证
改完参数不能只看单次效果,必须闭环验证:
- 业务层优先:比如发现大量String.substring()产生冗余char[],改用String.valueOf()或直接用原字符串切片,比调大堆更有效;
- JVM参数调优有前提:只有确认是GC策略或内存分配不合理导致瓶颈时才动参数,且每次只改一项(如先换G1,再调-XX:MaxGCPauseMillis,不同时改多个);
- 压测必做:用JMeter或wrk模拟真实流量,对比修改前后TPS、RT、GC次数、STW时间,避免“参数好看但业务扛不住”;
- 监控基线更新:调优后把新指标设为告警阈值,否则下次同样数值又会误报。











