静态变量是内存泄漏高频源头,因其生命周期与应用一致,若持有短命对象(如DTO、监听器),会阻止GC回收;静态集合类、单例内部状态、工具类缓存等易引发隐式强引用导致内存滞留。
直接看堆里谁被静态集合死死拽着不放——重点不是对象多,而是它们为什么一直活在老年代里。
确认泄漏现象:盯住老年代和Full GC效果
先别急着 dump,用 jstat -gc <pid> 持续观察 2–3 分钟:
- 如果 OU(老年代已使用)持续上涨,且每次 Full GC 后下降极少(比如只降 1–2MB),基本可锁定长周期泄漏
- 同时 FGC 频次升高但回收量不变,说明 GC 在反复清理年轻代,却对老年代“束手无策”
- 避免误判:确认没开 -XX:+UseG1GC 且没配错 Metaspace,排除类加载器或元空间干扰
定位静态集合:用 MAT 看 GC Root 路径
导出堆快照后,在 Eclipse MAT 中操作:
- 打开 Leak Suspects Report,优先看 “Problem Suspect 1” 是否指向某个
ArrayList、HashMap或自定义包装类的静态字段 - 进入 Dominator Tree,按包名排序,找体积最大的
java.util.ArrayList或你项目中常见的包装类(如UserWrapper、OrderCacheItem)实例 - 右键 → Path to GC Roots → exclude all weak/soft references,若终点是
java.lang.Class → static field,就坐实了——这个静态字段就是源头
验证包装类是否真被长期持有
不要只数对象个数,要看它们的“存活逻辑”:
- 检查该静态集合是否在业务循环中被不断
add(),但从未remove()、clear()或设限 - 确认包装类内部是否持有大对象引用(如
byte[]、String、嵌套集合),这些会放大泄漏影响 - 注意是否用了
static final Map<K, V>但 key 是普通对象(非弱引用)、value 又不可达——等效于强持整个链路
修复与验证:别只清内容,要断根
改代码时避开常见陷阱:
- 仅调用
list.clear()不够——如果其他单例 Bean 还持有该 list 引用,下次请求照样往里塞 - 换成
WeakHashMap也不保险——key 弱引用失效后 entry 被移除,但 value 若是包装类且自身强引用了别的资源,照样泄漏 - 真正稳妥的做法:删掉静态集合,换 Caffeine 缓存,明确配置
maximumSize(1000)和expireAfterWrite(5, MINUTES) - 上线前必须压测验证:模拟真实流量跑满 30 分钟,用 jstat -gc <pid> 确认 OU 曲线平稳,不再爬升

















