核心是确认数组存活时间远超元素业务周期,形成gc不可达的强引用链;重点检查静态或单例中未清理的数组,用mat分析dominator tree中异常大的cache/holder类实例,结合jstat观察old gen阶梯上升验证,并优先采用lru、weakhashmap或clear()修复。

识别内存泄漏中“长生命周期数组不断堆积引用”的核心,是确认数组本身存活时间远超其中元素的业务需求周期,并已形成 GC 不可达路径上的强引用链。
重点检查静态或单例作用域内的数组声明
静态数组(static Object[]、static List
- 搜索代码中所有 private static final T[]、public static List> 等声明,逐个确认是否有 add、put、set 操作但无对应 remove、clear 或过期淘汰逻辑
- 特别关注工具类(如 StringUtils)、全局事件总线(EventBus)、配置管理器(ConfigHolder)中的数组字段——它们常被默认当作“只读容器”,实则悄悄在后台写入监听器、会话数据或日志快照
- 用 MAT 打开 heap dump 后,在 Dominator Tree 中筛选类名含 “Cache”、“Buffer”、“Holder”、“Manager” 的实例,点开查看其 elementData(ArrayList)或 table(HashMap)大小是否异常(例如 >5000)
排查非静态但被长生命周期对象持有的数组
即使不是 static,只要数组属于 Spring 单例 Bean、Servlet 实例、连接池 Connection、或自定义线程池 Worker,它就具备“长命”属性。此时问题不在数组声明方式,而在使用后未重置。
- 检查 @Service 或 @Component 类中定义的 private List
pendingTasks ,确认每次请求处理完成后是否执行了 pendingTasks.clear() - 审查数据库连接内部的 PreparedStatement 缓存:若 SQL 是字符串拼接生成(如 "SELECT * FROM user WHERE id = " + userId),每条不同 userId 都会生成唯一缓存键,导致底层数组持续膨胀
- 留意 ThreadLocal
场景:若数组中存了 RequestScope 对象,且未在 finally 块中调用 threadLocal.remove(),该数组将随线程复用而越积越多
快速验证是否存在堆积型泄漏
不必等到 OOM,现在就能判断:
- 运行 jstat -gc
2000 ,观察老年代(Old Gen)使用率是否稳定上升;Full GC 后仅回落 3%~5%,基本可锁定为长生命周期引用堆积 - 执行 jmap -dump:format=b,file=heap.hprof
抓取堆快照,用 Eclipse MAT 打开 → 点击 Leak Suspects Report → 查看 “Problem Suspect 1” 是否指向某个数组及其大量重复实例(如 1000+ 个 UserSession) - 在关键写入点(如 list.add(obj))前后加日志,记录数组 size 变化;若 size 持续增长且无下降趋势,说明缺少清理入口
修复方向要匹配引用强度
不能只清空数组,得切断不该存在的强引用链:
- 优先加容量限制和淘汰策略:用 new LinkedHashMap(1000, 0.75f, true) 替代普通 HashMap,启用 LRU 自动驱逐
- 若元素天然支持弱引用语义(如监听器、上下文快照),改用 WeakHashMap
,让 Key 被 GC 时整条 Entry 自动失效 - 对必须长期持有的值,考虑包装为 SoftReference
,由 JVM 在内存紧张时自动回收 - 最直接有效的方式:在业务逻辑终点明确插入 arrayList.clear() 或 array = new Type[0],并确保该逻辑一定会被执行(建议放在 finally 或 @PreDestroy 中)










