排查缓存内存泄漏需聚焦缓存对象是否长期滞留无法回收:一级缓存紧盯SqlSession生命周期,避免复用过久;二级缓存检查Key/Value设计与无界配置;用MAT分析强引用锁死对象;慎用@Cacheable避免大对象缓存与序列化问题。

排查这类问题,核心是确认缓存对象是否长期滞留、无法被垃圾回收,而不是只看堆内存用了多少。现象通常是:应用不报错、GC能运行、但堆内存曲线缓慢持续上升,尤其在长时运行或定时任务后更明显。
盯住一级缓存的 SqlSession 生命周期
MyBatis 一级缓存默认开启,作用域是单个 SqlSession。若 SqlSession 复用过久(比如被手动管理、未及时 close)、或在长循环/长事务中反复执行相同查询,结果对象就会堆积在 PerpetualCache 的 HashMap 中,无法释放。
- 检查是否手动调用 sqlSession.close() 或 commit()/rollback() —— 这些操作会清空一级缓存;未执行则缓存一直挂着
- 避免在 for 循环里复用同一个 SqlSession 执行大量 select,尤其返回大 List 时;应改用批处理或分页 + 新建 SqlSession
- 不要依赖“自动清空”:update/delete 确实会清一级缓存,但仅限于当前 SqlSession;若没做修改操作,缓存就永远不退场
检查二级缓存的 Key/Value 设计与配置
二级缓存(如 、Caffeine、Ehcache)跨 SqlSession 共享,一旦 Key 冲突或 Value 引用外泄,极易造成内存泄漏。
-
Key 不规范:用 new Object()、含业务字段的复杂对象作 Key,又没重写
equals/hashCode,导致大量重复 Key 实例堆积 - Value 带隐式引用:缓存 VO/DTO 中持有 Service、Mapper、ThreadLocal、甚至 HttpServletRequest,使整个对象图无法回收
-
配置无界:如
timeToLiveSeconds=0(永不过期)、maxEntriesLocalHeap未设上限,数据只进不出
用 MAT 定位强引用锁死的缓存对象
拿到 .hprof 文件后,在 MAT 中聚焦缓存相关类,快速识别异常驻留对象:
- 打开 Histogram,筛选
org.apache.ibatis.cache、org.springframework.cache、com.github.benmanes.caffeine等包,看实例数和 Retained Heap 是否异常高 - 对可疑类右键 → Merge Shortest Paths to GC Roots → 勾选 exclude weak/soft references:这能过滤掉正常缓存项,暴露出被静态 Map、单例 CacheManager 或 ThreadLocal 强引用锁死的对象
- 查看 Dominator Tree,按 Retained Heap 排序,找排前列但业务上不该长期存在的对象(如某次查出的 50 万条 Entity 列表),再追溯其 GC Roots,大概率指向某个失控缓存
验证 Spring Cache 抽象层是否误用
@Cacheable 是常见泄漏点,尤其在代理机制和返回值设计上容易踩坑:
- 方法返回大对象(如
List)且未配unless或条件淘汰逻辑,缓存体积大、命中率低,还可能因 CGLIB 代理引入残留的 AOP 对象 - 缓存 Key 使用了未实现
Serializable的类(尤其在 Redis + Spring Cache 场景下),序列化失败可能导致异常缓存行为 - @CacheEvict 没配
allEntries = true或未覆盖关联命名空间,更新操作后旧缓存仍残留









