全局缓存是java内存泄漏最常见且隐蔽的源头,表现为老年代持续高位、full gc频繁但回收少、缓存容器实例占比高,根源多为只写不删、key强引用外部长生命周期对象或静态map与spring bean混用,修复需替换为weakhashmap/caffeine或加显式清理。

全局缓存是Java内存泄漏最常见也最隐蔽的源头之一。它不一定会立刻OOM,但会让堆内存像“慢性失血”一样持续上涨——重启后短暂回落,几小时内又逼近阈值,接口变慢、Full GC越来越频繁,这才是典型信号。
先确认是不是缓存惹的祸
别急着看代码,先用JVM原生命令快速交叉验证:
-
jstat -gc
2000 :重点盯 O(老年代使用率) 和 FGC(Full GC次数)。若O长期 >85% 且每次FGC后只下降几个百分点,基本锁定缓存类对象滞留 -
jmap -histo
| head -20 :查实例数最多的类。如果HashMap、ConcurrentHashMap或你自定义的CacheManager排前三,且总大小占堆30%以上,就是高危目标 - 翻GC日志,找 “GC overhead limit exceeded” 或连续多次 Full GC 耗时 >1s 的段落——这说明GC在反复清理无效缓存,却清不干净
定位缓存对象的强引用链
生成Heap Dump后,用MAT或VisualVM打开,直接看 Dominator Tree:
- 找到占用内存最大的缓存容器(比如
com.xxx.cache.GlobalCache.map),右键 → “Path to GC Roots” → 勾选 with all references - 重点看引用链顶端是不是 static 字段、Spring单例Bean的成员变量 或 ClassLoader —— 这些都是“钉子户”,让整个缓存树无法回收
- 如果路径里出现
java.util.TimerThread或java.lang.Thread持有缓存引用,说明后台清理线程没起作用,或清理逻辑本身有缺陷
检查缓存设计的三个致命漏洞
90%的全局缓存泄漏,逃不出以下三类问题:
- 只写不删:缓存put后从不remove,也没有LRU淘汰或过期机制。哪怕用了ConcurrentHashMap,只要key不被回收,value就永远钉在堆里
-
Key强引用外部长生命周期对象:比如用
User实体类做key,而User里有Hibernate代理、数据库连接或大字节数组——整个对象图都被拖住 - 静态Map + Spring Bean混合使用:Bean被Spring管理,但缓存Map声明为static,导致Bean销毁了,Map还活着,里面的数据全成孤儿
修复方案要落地,不能只讲理论
针对不同场景,给出可立即生效的改法:
- 替换静态Map:
private static final Map<k> cache = new WeakHashMap();</k>(适合key本身会自然失效的场景) - 引入Caffeine:
Cache<string object> cache = Caffeine.newBuilder().maximumSize(10_000).expireAfterWrite(30, TimeUnit.MINUTES).build();</string>(推荐,自动驱逐+异步清理) - 若必须用static集合,加显式清理入口:
@PreDestroy public void cleanup() { cache.clear(); },并确保该Bean是单例且能触发销毁钩子 - Key改造:不用业务实体,改用轻量ID字符串;或重写
equals/hashCode,确保不带冗余字段参与计算
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











