强引用缓存失控是内存泄漏主因:缓存未设上限、无淘汰机制、不清空,致对象长期滞留老年代,引发Full GC频繁且无效、响应卡顿。确认需先区分内存泄漏与溢出,典型信号为堆内存缓慢爬升、重启后快速复现、Full GC后老年代不回落或回落极小。

直接看堆内存里“该删不删”的对象是否越积越多——强引用缓存没设上限、没配淘汰、没清空,就会把本该回收的对象死死拽住,GC Roots 一拉到底,导致老年代持续膨胀、Full GC 频繁但收效甚微,最终系统响应卡顿甚至假死。
确认是不是缓存引发的泄漏
先排除误判:内存泄漏 ≠ 内存溢出。假死前若出现以下组合,高度提示是强引用缓存失控:
- 堆内存使用曲线单边缓慢爬升,重启后短期内快速复现
- Full GC 后老年代内存不回落,或回落幅度极小(
- 应用吞吐正常、CPU 不高,但线程大量阻塞在 GC 或锁竞争上
- 日志中频繁出现 “No space left in old gen” 或 “Concurrent Mode Failure”
抓快照,定位缓存根因
别等 OOM 才动手。在内存占用达 70% 左右时,立刻触发堆转储:
- 用 jmap -dump:format=b,file=heap.hprof
获取快照 - 用 open-mem 或 Eclipse MAT 打开,重点看 Dominator Tree
- 排序“Retained Heap”,找排前三的类——大概率是你的缓存容器(如
HashMap$Node、ConcurrentHashMap$Node)或其 value 类型(如UserInfo、OrderDetail) - 右键该类 → “Merge Shortest Paths to GC Roots”,勾选 “exclude weak/soft references”,看谁在强持引用链
查代码,盯死缓存生命周期
强引用缓存本身不违法,违法的是没人管它。重点检查以下四点:
-
静态缓存容器:如
public static final Map<string object> CACHE = new HashMap();</string>—— 没清理入口、无大小限制、无过期逻辑,就是泄漏温床 -
Spring @Cacheable 未配参数:只写
@Cacheable("user"),没加cacheManager或sync=true,底层可能用了无界 ConcurrentMap - 手动 put 却忘了 remove:比如登录成功存 token,但登出/超时未显式清除;或监听器注册后没注销,缓存 key 绑定 listener 实例
- 缓存 value 持有外部强引用:例如缓存了一个 Service 实例或上下文对象,而该对象又引用了大量其他 Bean 或资源流
改方案,用对缓存工具和模式
不是不用缓存,而是换更安全的姿势:
- 优先用 Caffeine 替代手写 Map:支持
maximumSize()、expireAfterWrite()、weakKeys()/softValues() - 若必须用 Spring Cache,明确指定
cacheManager,并配置基于 Caffeine 的CacheManagerBean - 静态缓存改为 Spring 管理的 prototype 或 scoped bean,确保可销毁
- 大对象缓存(如图片、文件内容)改用磁盘或分布式缓存(Redis),避免堆内堆积

















