上周三下午,公司内部系统突然卡住,好几个同事反馈页面打不开,后台日志疯狂报 OOM(OutOfMemoryError)。我登录服务器一看,堆内存从平时的 600M 直接飙到接近 2G,GC 频率也高得吓人。这种堆内存使用突然升高的情况,在线上服务中并不少见,处理不好分分钟导致服务瘫痪。
先别急着重启,看看是不是这几类常见原因
最开始我也想直接重启应用“快速解决”,但经验告诉我,如果不找出根源,过几个小时还会再来一遍。于是先用 jstat -gc 和 jmap -histo 扫了一眼,发现大量 java.util.HashMap$Node 实例堆积,再结合业务逻辑一查,果然是某个缓存接口没加 TTL,数据越攒越多。
像这种堆内存突增,大概率是下面几种情况:
- 代码里有集合类不断添加对象但没清理(比如静态 Map 缓存)
- 大文件或批量数据处理时一次性加载进内存
- 缓存配置不当,比如 Redis 客户端本地缓存了太多 Key
- 存在内存泄漏,比如监听器没注销、线程池没关闭
- 外部依赖返回异常大数据量,没做校验
动手排查:用工具说话
第一步是抓堆转储快照。趁着内存还高,执行:
jmap -dump:format=b,file=heap.hprof <pid>
然后拿这个文件用 MAT(Memory Analyzer Tool)打开,看“Dominator Tree”直接就能看到占用最高的对象。如果是 byte[] 多,可能是文件上传或序列化问题;如果是各种 POJO 实例扎堆,就得顺着引用链往上查是谁在持有它们。
有时候等不到 dump 完成,可以先用 jstack 看看当前线程栈,有没有哪个接口一直在跑。有一次我们发现某个导出功能被误点成了“全量导出”,一口气查了几百万条记录,全部塞进 List 返回,堆当然扛不住。
预防比抢救更重要
现在我们的上线流程里,加上了几条硬性要求:
- 所有缓存必须设置过期时间,禁止裸写 static HashMap
- 批量接口强制分页,单次最多取 5000 条
- JVM 参数统一加上 -XX:+HeapDumpOnOutOfMemoryError,出问题自动留证据
- 监控系统接入堆内存和 GC 耗时,超过阈值立刻告警
有个小项目之前没人管,堆内存每天下午都涨一波。后来发现是定时任务在拉第三方数据,对方接口偶尔会返回全量数据包,而我们没做大小校验。加了个判断,超过 10MB 直接丢弃并告警,问题就消停了。
堆内存突然升高不可怕,可怕的是每次都靠重启蒙混过关。多看一眼日志,多抓一次 dump,慢慢就能摸清自己系统的“脾气”。