静态集合类持续添加对象却不清理是java中最典型内存泄漏源,表现为老年代内存单向增长、fgc后不下降、ygc频次升高,可通过jstat、jmap和mat定位到static field引用的arraylist或hashmap,修复需用专业缓存库或配清理机制。

静态集合类(如 static List 或 static Map)持续添加对象却不清理,是 Java 中最典型也最容易被忽视的内存泄漏源头。它不报错、不崩溃,只让老年代内存缓慢但坚定地上升,最终触发频繁 Full GC 甚至 OutOfMemoryError。
看现象:先确认是不是它在作怪
别急着翻代码,先用 JVM 自带工具验证异常模式:
-
jstat -gc <pid></pid>持续观察:重点关注OU(老年代已使用空间)是否单向增长,且每次FGC后几乎不下降 - 对比
OC(老年代容量)和OU的比值,若长期 >85% 并持续爬升,基本可锁定长生命周期对象堆积 - 同时检查
YGC频次是否异常升高——说明年轻代对象“逃逸”到老年代变多,而根源常是被静态集合长期持有着
抓快照:用 jmap 定位“谁在撑大堆”
确认异常后,立即导出堆快照(生产环境建议选低峰期,并预估暂停时间):
jmap -dump:format=b,file=heap.hprof <pid></pid>- 用 Eclipse MAT 打开,直接点 Leak Suspects Report —— 大概率第一嫌疑就是某个
java.util.ArrayList或java.util.HashMap实例 - 进入 Dominator Tree,按包名排序,找你项目里常见的工具类或缓存类;右键该集合 → Path to GC Roots → 勾选 exclude all weak/soft references
- 如果路径终点是
java.lang.Class → static field,就坐实了:这个静态字段就是泄漏根因
查代码:三类高危写法要立刻扫描
在工程中全局搜索 private static.*List、private static.*Map、static.*cache 等关键词,重点盯以下模式:
-
无上限无清理的裸集合:
private static List<user> cache = new ArrayList();</user>—— 添加方法有,清除逻辑全无 - 伪缓存:只有 put 没有 remove/expiry —— 尤其注意 Spring @Service 类里的 static Map,容易被误当“轻量缓存”滥用
-
ThreadLocal + static 组合:
private static final ThreadLocal<map> holder = new ThreadLocal();</map>—— 实际等效于多个线程各自持有一个强引用集合,且永不释放
修方案:别只清内容,要断引用链
修复不是简单加个 list.clear(),关键在于打破 GC Root 引用链并防止复发:
- 优先替换为专业缓存库:用 Caffeine(推荐)或 Guava Cache,配置
maximumSize和expireAfterWrite,自动淘汰 - 若必须手写,用
ConcurrentHashMap替代HashMap,配合定时任务调用computeIfPresent清理过期项(注意加锁或 CAS 避免并发问题) - 禁用
WeakHashMap作为通用解法——它只对 key 生效,value 仍是强引用;若 value 是大对象或持有其他引用,照样泄漏 - 所有静态集合必须配套提供
clearAll()或evictExpired()方法,并在应用关闭钩子(Runtime.addShutdownHook)中调用
不复杂但容易忽略:静态集合泄漏从不发生在某一行代码,而发生在“没人负责清理”的设计空白里。










