spring bean内存泄漏本质是生命周期不匹配,需通过jstat查老年代堆积、jmap+mat分析gc roots定位强引用源,并聚焦单例持无界集合、prototype被单例持有、web作用域bean误入异步线程、自定义scope未清理资源四类场景验证。

Spring 容器中 Bean 的内存泄漏,本质是 Bean 的生命周期与它所持对象的生命周期不匹配——该释放的没释放,该销毁的没销毁。排查不能只盯着“是不是 Bean”,而要盯住“谁在强引用它、为什么 GC 不收它”。
看老年代增长和 GC 行为,确认泄漏存在
运行中观察 jstat -gcutil <pid> 2000</pid>:
- 老年代(O 列)持续高于 90%,Full GC 后仅下降 2%~5%,且每次 GC 后基线更高
- FGC 频次明显增加,单次耗时超 1 秒
- Young GC 正常回收 Eden 区,说明问题不在新生代,而在“活下来”的对象
满足前两条,基本可判定是 Bean 相关对象在老年代堆积。
抓堆快照,用 MAT 找出被谁拽着不放
应用运行稳定后(非 OOM 瞬间),执行:
jmap -dump:live,format=b,file=heap.hprof <pid></pid>
用 Eclipse MAT 打开:
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
- 先看 Leak Suspects Report,MAT 常直接标出
ApplicationContext未关闭、SingletonBean持有HashMap等典型问题 - 再看 Dominator Tree,按 Retained Heap 排序,重点查:
-
HashMap、ArrayList、ConcurrentHashMap实例数异常多 - 自定义缓存类(如
UserCache、TokenStore)占用内存高
-
- 对可疑对象右键 → Path to GC Roots → 勾选 exclude weak/soft references
- 若路径终点是
static final字段、SpringApplication、ContextLoaderListener或某个@Service单例字段,就是 Spring Bean 泄漏的铁证
- 若路径终点是
聚焦四类高频 Bean 泄漏场景,逐项验证
单例 Bean 持有无界集合
如@Service类里写private Map<string object> cache = new HashMap()</string>,没过期、没大小限制、没并发清理
✅ 改用 Caffeine,配maximumSize(1000)和expireAfterWrite(10, TimeUnit.MINUTES)prototype Bean 被单例错误持有
如单例 Service 中@Autowired private PrototypeBean bean;—— Spring 只注入一次,后续永远复用
✅ 改为ObjectProvider<prototypebean></prototypebean>或ApplicationContext.getBean(PrototypeBean.class)动态获取Web 作用域 Bean 进入异步线程
如@Async方法里调用@RequestScopeBean,Spring 无法绑定上下文,可能返回旧实例并隐式强引用
✅ 异步任务中避免直接注入 request/session Bean;必须用时,通过RequestContextHolder手动传递参数自定义 Scope 或集成组件未清理资源
如@Scope("tenant")Bean 中创建了RedisConnection或OSSClient,但没声明@PreDestroy关闭逻辑
✅ 所有外部资源初始化后,必须配对应销毁方法,并确保容器能调用到
不复杂但容易忽略
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










