
凌晨两点手机疯狂震动。监控平台发来告警核心服务响应超时CPU飙到95%日志里赫然出现java.lang.OutOfMemoryError: Java heap space。我翻身下床打开电脑开始了这次OOM排查。第一步保留现场别急着重启很多运维的第一反应是重启但重启会丢失现场。我立刻登录服务器先确认进程还在然后执行bash复制下载jmap -dump:live,formatb,file/tmp/heap.hprof 12345同时检查JVM启动参数确认已经配置了-XX:HeapDumpOnOutOfMemoryError这样即使进程崩溃也能自动保存堆快照。接着用jstat -gcutil 12345 1000观察GC情况老年代使用率99%Full GC每隔几秒触发一次但回收效果几乎为零。典型的堆内存泄漏。第二步分析堆快照找到大对象把hprof文件下载到本地用MATMemory Analyzer Tool打开。首页的“Leak Suspects”报告直接指向一个ConcurrentHashMap它占了整个堆的78%里面有超过200万个Entry。展开看Key是用户IDValue是一个订单列表对象。很明显这是一个本地缓存但没有任何清理机制。继续用MAT的“Path to GC Roots”功能发现这个Map被一个静态变量持有生命周期和JVM一样长。代码里这样写的java复制下载private static final MapLong, ListOrder ORDER_CACHE new ConcurrentHashMap();每次查询订单如果缓存里没有就查数据库然后放进去。但从来没有移除逻辑也没有设置上限。系统运行了半个月缓存越来越大最终撑爆了堆。第三步定位代码确认泄漏路径回到代码仓库搜索ORDER_CACHE发现调用点在一个订单查询接口里java复制下载public ListOrder getOrders(Long userId) { ListOrder 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:MaxGCPauseMillis200。定期做堆dump分析防患于未然。第五步复盘与总结这次OOM的根本原因不是JVM参数没调好而是代码里埋了一个无界缓存。JVM调优能解决的是“资源怎么分配”但解决不了“对象该不该活”。排查OOM工具只是辅助核心是理解对象的生命周期。记住三点线上必须开启HeapDumpOnOutOfMemoryError现场比什么都重要。MAT是分析堆的利器重点看“Leak Suspects”和“Dominator Tree”。本地缓存一定要有界最好用成熟框架别自己手写Map。那次之后我们组定了一条规矩任何静态集合必须写明清理策略否则代码评审不通过。毕竟JVM再能扛也扛不住你无限往里塞对象。