排查缓存导致堆内存持续增长,关键确认缓存对象是否长期滞留无法回收;重点检查mybatis一二级缓存配置与使用规范、key/value设计、过期策略缺失及spring cache误用,并通过mat分析引用链与运行时监控交叉验证。

排查持久化层一二级缓存管理失控导致的堆内存持续增长,核心在于确认“缓存对象是否长期滞留、无法被回收”,而非简单看内存用了多少。这类问题隐蔽性强——应用不报错、GC能运行、但堆内存曲线像爬坡一样缓慢上扬,尤其在长时间运行或定时任务触发后更明显。
盯住缓存类对象的生命周期和引用链
一级缓存(如 MyBatis 的 SqlSession 级别缓存)和二级缓存(如 Ehcache、Caffeine、Redis 客户端本地缓存)若配置不当或使用不规范,极易成为内存泄漏温床。重点检查:
- MyBatis 一级缓存未及时清空:SqlSession 复用过久、未手动调用 clearCache(),或在长事务/循环中反复执行相同查询,导致结果集对象堆积在 SqlSession 内部的
PerpetualCache中; - 二级缓存 Key 或 Value 无界增长:例如用
new Object()或含业务实体的复杂对象作缓存 Key,未重写equals/hashCode,造成大量重复 Key 实例;又或 Value 是未序列化/未清理的 VO/DTO,含对 Service、Mapper、甚至 ThreadLocal 的隐式引用; - 缓存未设置过期策略或最大容量:比如 Ehcache 配置了
timeToLiveSeconds=0(永不过期)且maxEntriesLocalHeap未设上限,数据越积越多; - Spring Cache 抽象层误用:
@Cacheable方法返回值为大对象(如 List),且未配置 unless或条件淘汰逻辑,缓存命中率低但体积大,还可能因代理机制引入 AOP 相关的代理对象残留。
用 MAT 快速定位缓存相关对象
拿到堆转储(.hprof)后,在 MAT 中直接聚焦缓存行为特征:
- 打开 Histogram,按包名筛选:
org.apache.ibatis.cache、org.springframework.cache、com.github.benmanes.caffeine、net.sf.ehcache等,看对应类实例数和保留集(Retained Heap)是否异常高; - 右键可疑类 → Merge Shortest Paths to GC Roots → exclude weak/soft references:这能过滤掉正常弱引用缓存项,暴露出那些被强引用锁死、无法回收的对象路径;常见罪魁是静态 Map、单例 CacheManager 持有未清理的 Entry、或线程局部变量(ThreadLocal)意外持有缓存句柄;
- 查看 Dominator Tree,排序 Retained Heap,找排在前列但业务上不该长期存在的对象类型(如某次批量查询返回的 50 万条 Entity 列表),再追溯其 GC Roots,大概率指向某个缓存容器或静态上下文。
结合运行时行为做交叉验证
仅靠 dump 文件容易误判,需辅以动态观测:
- 开启 MyBatis 日志(
logging.level.org.apache.ibatis=DEBUG),观察同一 SQL 是否高频命中一级缓存(log 中出现Cache Hit),同时检查该 SqlSession 是否被长期复用(如注入到单例 Service 中); - 对二级缓存组件启用统计开关:如 Caffeine 加
recordStats(),暴露hitRate()、estimatedSize()等指标,通过 Actuator / Prometheus 拉取,确认缓存是否“只进不出”; - 用 jcmd
VM.native_memory summary 对比堆内与堆外内存增长趋势——若堆内存涨得快但堆外平稳,基本可排除 Netty Direct Buffer 类问题,进一步锁定是 Java 对象层缓存失控; - 临时加监控点:在关键缓存 put 操作前后打印
Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(),确认单次写入是否引发不可逆的内存抬升。
代码与配置层面的关键修复点
确认问题后,修复要直击根源,而非仅调大堆内存:
- 一级缓存:避免跨请求复用 SqlSession;必要时在方法末尾显式
sqlSession.clearCache();慎用flushCache=true的更新语句,防止误清其他缓存; - 二级缓存:强制 Key 实现规范的
equals/hashCode;Value 尽量用轻量 DTO,避免携带 Spring 上下文或数据库连接;配置合理maxSize和expireAfterWrite; - 禁用高风险默认行为:如 MyBatis 默认开启一级缓存,若业务明确不需要,可在配置中设
localCacheScope=STATEMENT;Spring Boot 中关闭自动配置的SimpleCacheManager,改用显式声明带限流策略的缓存器; - 增加缓存健康检查:定期调用
Cache.getNativeCache()获取底层实例,校验 size、hit rate 是否超出阈值,并记录告警。










