缓存有效期管理失控是内存持续攀升的隐蔽诱因,表现为老年代长期高位、Full GC 频繁且收效甚微;核心在于本该回收的缓存对象滞留堆中。

缓存有效期管理失控,是内存持续攀升最隐蔽也最典型的诱因之一。它不一定会立刻触发 OOM,但会让堆内存缓慢“爬坡”,老年代长期高位、Full GC 频繁却收效甚微——问题往往就出在“该过期的没过期,该淘汰的没淘汰”。
核心判断标准不是“用了多少内存”,而是“缓存对象是否本该被回收却一直钉在堆里”。
一、先确认是不是有效期惹的祸
别急着翻代码,用 JVM 原生命令快速交叉验证:
jstat -gc2000
关注O(老年代使用率)和FGC(Full GC 次数)。若O长期 >85%,每次 FGC 后只回落 1–3%,说明大量缓存对象滞留。jmap -histo| head -20
看实例数最多、总大小靠前的类:
✅ 高危信号:ConcurrentHashMap$Node、LinkedHashMap$Entry、Caffeine.Node、自定义CacheHolder.map;
❌ 警惕:这些容器本身不占大头,但它们的value(比如UserVO[]、OrderDetailList)可能占堆 40% 以上。查 GC 日志
找到连续多段Full GC耗时 >800ms 且After GC内存下降极少的日志,基本可锁定缓存未释放。
二、重点排查三类“永不过期”场景
真正让缓存变“僵尸”的,往往是配置或设计上的硬伤:
-
TTL 被设为 0 或 -1
- MyBatis 二级缓存:
中flushInterval=0≠ 自动刷新,而是禁用定时清空; - Redis 客户端(如 Jedis/Lettuce):
set(key, value, 0, TimeUnit.SECONDS)表示永不过期; - Caffeine 构建时
.expireAfterWrite(0, SECONDS)同样等于关闭过期。
- MyBatis 二级缓存:
-
过期策略存在但未生效
- Spring Cache 的
@Cacheable(unless = "#result == null")写错逻辑,导致空结果也被缓存且无 TTL; - 使用了
@Cacheable(sync = true)但底层缓存实现不支持同步锁,引发重复加载+重复缓存; - 缓存 key 设计含时间戳、随机 UUID 等,使同一业务数据生成无数个 key,根本无法命中,更谈不上过期。
- Spring Cache 的
-
过期依赖外部机制,但机制失效
- 用数据库 binlog 清缓存,但 Canal 消费延迟或丢事件;
- 用消息队列通知失效,但消费者宕机未重试;
- 本地缓存(如 Guava/Caffeine)配了
expireAfterAccess,但业务线程永远在访问(比如心跳接口轮询),导致“永远新鲜”。
三、用 MAT 快速定位“赖着不走”的缓存项
拿到 .hprof 后,在 MAT 中按顺序操作:
- 打开 Dominator Tree → 按
Retained Heap排序 → 找到占用最大的缓存容器(如com.xxx.cache.UserCache.map) - 右键该 Map → Path to GC Roots → 勾选 with all references(不要 exclude)
- 重点看引用链顶端:
- 若是
static final字段 → 缓存生命周期与类绑定,无法随 Bean 销毁; - 若是 Spring 单例 Bean 的成员变量 → Bean 存活,缓存就一直活着;
- 若是
ThreadLocal或TimerThread→ 清理线程卡住或未启动。
- 若是
再打开 Histogram → 过滤 org.springframework.cache 或 com.github.benmanes.caffeine → 看 LoadingCache 或 BoundedLocalCache 实例数是否异常高(>1000 个),结合 Retained Heap 判断是否堆积。
四、修复要具体,不能只改配置
-
立即生效的改法
- 把
expireAfterWrite(0, SECONDS)改成expireAfterWrite(30, MINUTES); - MyBatis 二级缓存加
size="1000"+eviction="LRU",禁用flushInterval(改由业务事件驱动); - Spring Cache 方法上补
@Cacheable(..., cacheManager = "shortLivedCache"),并单独配一个带 TTL 的CacheManager。
- 把
-
兜底机制必须有
- 所有本地缓存加
maximumSize(5000),避免无限膨胀; - Redis 缓存统一加
EXPIRE或用SET key value EX 1800命令写入; - 关键缓存加健康检查:定时扫描
KEYS user:*+TTL,告警 TTL >24h 的 key。
- 所有本地缓存加
不复杂,但容易忽略。











