JVM调优实战:一次线上OOM的排查全过程
2026/9/15 6:36:05 网站建设 项目流程

凌晨两点,手机疯狂震动。监控平台发来告警:核心服务响应超时,CPU飙到95%,日志里赫然出现java.lang.OutOfMemoryError: Java heap space。我翻身下床,打开电脑,开始了这次OOM排查。

第一步:保留现场,别急着重启

很多运维的第一反应是重启,但重启会丢失现场。我立刻登录服务器,先确认进程还在,然后执行:

bash
复制
下载
jmap -dump:live,format=b,file=/tmp/heap.hprof 12345

同时检查JVM启动参数,确认已经配置了-XX:+HeapDumpOnOutOfMemoryError,这样即使进程崩溃也能自动保存堆快照。接着用jstat -gcutil 12345 1000观察GC情况:老年代使用率99%,Full GC每隔几秒触发一次,但回收效果几乎为零。典型的堆内存泄漏。

第二步:分析堆快照,找到大对象

把hprof文件下载到本地,用MAT(Memory Analyzer Tool)打开。首页的“Leak Suspects”报告直接指向一个ConcurrentHashMap,它占了整个堆的78%,里面有超过200万个Entry。展开看,Key是用户ID,Value是一个订单列表对象。很明显,这是一个本地缓存,但没有任何清理机制。

继续用MAT的“Path to GC Roots”功能,发现这个Map被一个静态变量持有,生命周期和JVM一样长。代码里这样写的:

java
复制
下载
private static final Map<Long, List<Order>> ORDER_CACHE = new ConcurrentHashMap<>();

每次查询订单,如果缓存里没有,就查数据库然后放进去。但从来没有移除逻辑,也没有设置上限。系统运行了半个月,缓存越来越大,最终撑爆了堆。

第三步:定位代码,确认泄漏路径

回到代码仓库,搜索ORDER_CACHE,发现调用点在一个订单查询接口里:

java
复制
下载
public List<Order> getOrders(Long userId) { List<Order> orders = ORDER_CACHE.get(userId); if (orders == null) { orders = orderDao.queryByUserId(userId); ORDER_CACHE.put(userId, orders); } return orders; }

问题很清晰:用户量上百万,每个用户都缓存,而且订单数据还会变化,缓存永远不会失效。这是典型的“把缓存当数据库用”,而且没有淘汰策略。

第四步:紧急修复与长期方案

紧急处理:先重启服务,临时把堆从2G调到4G,争取时间。然后修改代码,把本地缓存换成Redis,并设置TTL。如果非要用本地缓存,至少用Caffeine,指定maximumSize(10000)expireAfterWrite(10, TimeUnit.MINUTES)

长期方案:

所有本地缓存必须设置容量上限和过期时间。

增加监控:对缓存大小、GC频率、老年代使用率设置告警。

JVM参数优化:-Xms-Xmx设为相同值,避免动态扩容;-XX:+UseG1GC,并设置-XX:MaxGCPauseMillis=200

定期做堆dump分析,防患于未然。

第五步:复盘与总结

这次OOM的根本原因不是JVM参数没调好,而是代码里埋了一个无界缓存。JVM调优能解决的是“资源怎么分配”,但解决不了“对象该不该活”。排查OOM,工具只是辅助,核心是理解对象的生命周期。

记住三点:

线上必须开启HeapDumpOnOutOfMemoryError,现场比什么都重要。

MAT是分析堆的利器,重点看“Leak Suspects”和“Dominator Tree”。

本地缓存一定要有界,最好用成熟框架,别自己手写Map。

那次之后,我们组定了一条规矩:任何静态集合,必须写明清理策略,否则代码评审不通过。毕竟,JVM再能扛,也扛不住你无限往里塞对象。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询