Java日常巡检实战案例:从Full GC风暴到线程阻塞的7个排查路径

目录导读
- 巡检前的准备:监控指标与工具链清单
- Full GC频率突增——JVM堆内存泄漏的定位
- 接口RT飙升——线程池阻塞与死锁诊断
- 数据库连接池耗尽——连接泄露的Log分析
- 巡检自动化:脚本+告警的闭环设计
- Q&A高频问答:解决你巡检中的真实困惑
巡检前的准备:别把“巡检”做成“救火”
在日常运维中,最怕的是“平时不烧香,急时抱佛脚”,一次合格的Java巡检,应当在业务低峰期(如凌晨2点)执行,并采集以下黄金指标:
- JVM层:堆内存使用率(老年代占比)、GC暂停时间、存活对象大小。
- 线程层:活跃线程数、BLOCKED/WAITING状态线程分布、锁等待队列长度。
- 中间件层:Tomcat/Netty的线程池活跃度、数据库连接池活跃链接数。
工具建议:jstat -gcutil 查看GC曲线,jstack 抓取线程快照,Arthas 在线诊断,同时开启JMX远程监控,并用Prometheus+Grafana实现可视化。
案例一:Full GC频率从1小时/次飙升至5分钟/次
现象:告警系统提示Full GC次数异常,且老年代内存回收后仍持续上涨。
排查步骤:
- 执行
jmap -histo:live <pid>查看存活对象Top10,发现byte[]和LocalDateTime对象数量异常。 - 通过
jmap -dump:format=b,file=heap.hprof导出堆快照,用MAT分析支配树,定位到某业务线程持有的 静态HashMap 未清理,且Key为请求IP(导致永不失效)。 - 修复措施:改用
Caffeine本地缓存并设定过期策略,同时增加-XX:+UseG1GC避免内存碎片。
启示:巡检时应当重点关注老年代占用率曲线,若连续3次降不下去,基本判定泄漏。
案例二:订单接口P99从200ms暴涨至5s
现象:压测时发现线程池队列积压,但CPU使用率仅15%。
诊断路径:
- 执行
jstack <pid>三次,间隔10秒,发现同一业务锁OrderLock上有大量BLOCKED线程。 - 进一步查看代码,发现同步块内调用了 外部HTTP服务,且未设置超时时间(默认无限等待)。
- 解决方案:改用
ReentrantLock.tryLock(2s),并将HTTP调用改为异步化,同时启用Hystrix熔断。
巡检建议:每次巡检需抓取 jstack 两次以上,对比线程状态是否持续卡死;同时检查 JVM 参数 -Djava.net.preferIPv4Stack=true 是否设置,避免DNS解析延迟。
案例三:数据库连接池从50个连接耗尽
现象:应用日志报 Cannot get a connection, pool exhausted,但DB侧活跃连接数并不高。
根因:某定时任务中 Connection 未在 finally 块中关闭,且 HikariCP 的 maxLifetime 小于MySQL的 wait_timeout 导致连接被DB回收后应用仍持有。
巡检技巧:
- 使用
SHOW PROCESSLIST对比应用侧连接数。 - 在代码中启用
HikariCP的leakDetectionThreshold(如10秒)自动检测泄露SQL。 - 通过
Btrace或Arthas的trace命令动态追踪getConnection的调用栈。
巡检自动化:将人力从“看图表”中解放
成熟团队会编写Shell/Python脚本,实现:
- 每日凌晨自动采集
jstat,jstack,gc.log。 - 用
awk统计GC累计暂停时间,若超过300ms则触发邮件告警。 - 利用
ElasticSearch存储历史快照,实现趋势对比(如“本周FullGC次数较上周上升30%”)。
关键检查项:磁盘空间(/tmp 是否被heap dump占满)、文件句柄数(lsof | wc -l 是否接近 ulimit 限制)。
Q&A高频问答
Q1:巡检中发现Metaspace一直在涨,但回收不掉,怎么办?
A:先用 jstat -gcmetacapacity 查看扩容情况,大部分原因是CGLIB代理类或反射生成类过多,建议检查代码是否大量使用Proxy.newProxyInstance(),并设置 -XX:MaxMetaspaceSize 限制,同时配合 -XX:+TraceClassLoading 定位触发源。
Q2:如何区分“正常波动”和“故障前兆”?
A:推荐“3σ原则”——建立7天基线(如CPU均值30%,标准差5%),当监控值超过均值+3倍标准差时触发告警,对GC而言,正常CMS的remark阶段暂停应在50ms内,若超过则视为异常。
Q3:巡检时发现 Java 17 相比 Java 8 的线程调度差异,需要调整参数吗?
A:重点检查两点:-XX:+UseZGC 或 -XX:+UseShenandoahGC 是否适合低延迟场景;Java 17的默认GC是G1,但需要显式设置 -XX:MaxGCPauseMillis=200,另外Java 17的Thread.onSpinWait() 在自旋锁场景下显著降低CPU占用。
Java日常巡检不是“点开JConsole看一眼”,而是对JVM、线程、网络、代码逻辑的系统性审计,通过上述案例,希望你能建立一套可复用的巡检SOP——先看指标、再抓线程、后析堆栈、最后改代码,每一次Full GC的背后,都藏着一个未被释放的引用;每一次RT的飙升,都有一行未设超时的代码,巡检的价值在于提前发现这些“隐形定时炸弹”,而非事后firefighting。