
凌晨两点监控告警骤然响起某核心服务响应时间飙升接口大量超时日志中频繁出现java.lang.OutOfMemoryError: Java heap space。作为值班开发我立刻从床上爬起开启了一次与 OOM 的正面交锋。一、保留现场快速止血第一原则不要急着重启。重启会丢失堆内存现场让排查变成无头悬案。我迅速登录服务器执行jps -l找到 Java 进程 PID随后用jmap -dump:formatb,fileheap.hprof pid导出堆快照。同时为了尽快恢复服务我临时将节点从负载均衡中摘除并适当调大-Xmx重启先保证业务可用。保留的 dump 文件成为后续破案的关键。二、分析 GC 日志锁定异常我在启动参数中早已配置了 GC 日志-Xloggc:gc.log -XX:PrintGCDetails -XX:PrintGCDateStamps。查看日志发现Full GC 从平时的几小时一次变成几分钟一次且每次回收后老年代占用仅下降几个百分点典型的“内存泄漏”特征——对象被持续引用无法回收。这排除了单纯的内存不足指向了代码层面的泄漏。三、堆直方图初现端倪使用jmap -histo:live pid查看存活对象直方图发现java.util.HashMap$Node数量高达数百万远超正常水平。同时com.xxx.entity.UserSession实例也异常多。初步判断某个缓存或会话容器没有清理机制导致对象不断堆积。四、MAT 深度分析定位根因将 dump 文件导入 Eclipse MAT打开支配树Dominator Tree。发现一个ConcurrentHashMap占据了超过 80% 的堆内存其内部持有大量UserSession对象。继续追踪引用链发现该 Map 被一个静态变量SessionManager.cache引用。查看代码这是一个本地会话缓存用于存储用户登录信息但没有设置过期时间也没有容量上限。随着用户量增长缓存只增不减最终撑爆堆内存。五、解决方案与调优定位根因后修复方案分三步代码修复引入Caffeine或Guava Cache设置maximumSize(10000)和expireAfterAccess(30, TimeUnit.MINUTES)实现自动淘汰。JVM 参数调优增加-XX:HeapDumpOnOutOfMemoryError和-XX:HeapDumpPath/data/dump确保下次 OOM 自动保留现场。同时将-Xmx从 2G 调整到 4G并设置-Xms与-Xmx相同避免堆动态伸缩带来的性能抖动。年轻代采用-XX:NewRatio2让短命对象尽快在 Young GC 中回收。监控加固接入 Prometheus Grafana监控老年代使用率、Full GC 频率和耗时设置阈值告警。上线后观察一周Full GC 恢复到每天一次以内老年代内存曲线平稳OOM 再未出现。六、总结与反思这次排查让我深刻体会到OOM 往往不是 JVM 参数配得不好而是代码写了“内存泄漏”。调优的前提是理解业务与内存模型。几点经验值得铭记任何本地缓存都必须有淘汰策略否则就是定时炸弹。生产环境务必开启HeapDumpOnOutOfMemoryError现场比黄金珍贵。熟练掌握jmap、jstat、MAT 等工具能让你在深夜排查时少走弯路。GC 日志是 JVM 的“体检报告”坚持分析异常早发现。JVM 调优不是玄学而是一套基于证据的方法论。从现象到日志从直方图到引用链每一步都指向真相。希望这次实战经历能为你下一次与 OOM 的遭遇战提供一份可靠的作战地图。