昨天下午三点,公司内部系统突然卡住,接口响应从 200ms 暴涨到 5 秒以上。运维同事一查监控,发现 Java 应用的堆内存使用率瞬间冲到 95%,GC 频率明显上升。这种堆内存使用突然升高的情况,在网络应用中并不少见,尤其发生在流量平稳的情况下,更让人摸不着头脑。
是不是有大对象被频繁创建?
有个团队上线了个新功能:每次请求都会把整个用户行为日志加载进内存做实时分析。日志文件动辄几十 MB,请求一多,堆内存蹭蹭往上涨。后来改成分段处理 + 流式解析,问题立马缓解。如果你的应用最近加了类似“全量加载”的逻辑,不妨先检查一下。
缓存没设上限,内存越积越多
本地缓存用的是 ConcurrentHashMap 自己实现的,没加过期策略也没限制大小。结果某个热点数据不断写入,缓存越攒越多,最终拖垮堆内存。换成 Caffeine 并设置最大容量后,内存使用变得平稳多了。
Cache<String, Object> cache = Caffeine.newBuilder() .maximumSize(10000) .expireAfterWrite(10, TimeUnit.MINUTES) .build();
连接池或资源泄漏
有一次发现堆内存缓慢上涨,重启后立刻恢复正常。抓取堆 dump 分析,发现大量 HttpConnection 实例未释放。原来是某次调用外部 API 时,异常处理不当导致连接未关闭。这类资源泄漏初期不明显,但积累一段时间后会集中爆发。
GC 参数不合理,回收不及时
默认的 GC 策略不一定适合你的应用。比如用了 G1GC,但堆设置过大(超过 8G),而暂停时间目标又太激进,可能导致年轻代回收频繁却效果差。适当调整 -XX:MaxGCPauseMillis 或切换到 ZGC 可能更合适。
第三方库偷偷搞事情
引入了一个新的 JSON 解析库,表面上挺好用,结果在线上跑了一周后内存飙升。反编译一看,内部用了静态缓存存放解析模板,且永不清理。换回 Jackson 后问题消失。集成新依赖时,别只看功能,也得留意它的内存行为。
怎么快速定位?
第一步是看监控图,确认升高是突变还是渐进。然后用 jstat -gc 观察 GC 频率和回收量。如果怀疑泄漏,用 jmap 导出堆 dump,配合 Eclipse MAT 或 JVisualVM 查看哪些类实例最多。也可以开启 JFR 记录一段时间内的对象分配情况。
线上遇到堆内存突然升高,别急着重启。先保留现场,收集日志、dump 和 GC 日志,才能真正找到根因。很多时候,问题就藏在一次看似无害的代码提交里。