MemoryCache 不会自动释放内存,因过期仅标记“可回收”,实际释放依赖GC和主动清理;需定时调用Compact()或Remove(),避免RSS持续上涨。

为什么 MemoryCache 不会自动释放内存,光靠过期时间不够
缓存项过期只是标记为“可回收”,实际内存释放依赖后台 GC 和缓存内部的清理策略。尤其在高吞吐、小对象高频写入场景下,MemoryCache 可能长期持有大量已过期但未被扫描到的条目,导致 RSS 持续上涨。这不是 bug,是设计使然——它优先保障访问性能,不主动触发 GC。
实操建议:
- 必须显式调用
Compact()或Remove()触发清理,不能只等过期 -
Compact()是最轻量的主动清理方式,按权重移除旧/低优先级项,推荐每 5–10 分钟调用一次 - 避免在
GetOrCreateAsync回调里直接写入大量新缓存——这会加剧堆积,应先Compact()再写入 - 注意:.NET 6+ 的
MemoryCache默认启用后台扫描(ExpirationToken类型触发),但扫描频率不可控,仍需人工干预
用 CancellationToken 安全终止定时清理任务,避免 ObjectDisposedException
常见错误现象:Task.Run(() => cache.Compact(0.75)).Wait(cancellationToken) 在服务停止时抛出 ObjectDisposedException,因为 Compact() 内部可能访问已 Disposed 的同步上下文或计时器资源。
正确做法是把清理逻辑包进可取消的循环中,而非对单次 Compact() 做超时等待:
- 用
PeriodicTimer(.NET 6+)或Timer+cancellationToken.Register()管理周期性调度,不是用Task.Delay(..., token)做轮询 -
Compact()本身不接受CancellationToken,所以清理动作必须是同步且快速的(毫秒级),否则取消信号会延迟响应 - 在
Dispose()里先调用timer.Dispose(),再调用一次cache.Compact(1.0)强制清空,确保无残留 - 不要在
using块里启动定时器——缓存实例生命周期通常长于单个请求,该由宿主(如BackgroundService)统一管理
MemoryCache 清理时机选 Compact() 还是手动 Remove()
两者适用场景完全不同:Compact() 是批量、启发式、低开销的内存压力响应;Remove() 是精确、即时、可控的单点清理。混用容易引发重复删除或漏删。
实操建议:
- 定时任务统一用
Compact(0.5–0.75):数值越接近 1.0 清得越狠,但耗时越长;0.5 是较稳妥的平衡点 - 业务层主动失效(如订单更新后清对应缓存)必须用
Remove(key)或带PostEvictionCallbacks的Set(),不能依赖定时Compact() -
Compact()不会触发PostEvictionCallbacks,如果清理逻辑依赖回调(如日志、统计),得自己封装一层带回调的清理方法 - 若缓存项实现
IDisposable,Compact()不会调用Dispose()—— 必须在PostEvictionCallbacks里手动处理
.NET 5 与 .NET 6+ 在缓存清理上的关键差异
主要影响兼容性和默认行为:.NET 5 的 MemoryCache 没有 Compact() 方法,也没有 PeriodicTimer,强行升级需适配。
迁移要点:
- .NET 5 项目必须手动实现类似
Compact()的逻辑:遍历cache.TryGetValue()找过期项 +Remove(),性能差、易锁表 - .NET 6+ 推荐用
IMemoryCache+PeriodicTimer组合,比老式Timer更省资源、取消更及时 - 配置上,
SizeLimit在 .NET 6+ 中真正参与Compact()权重计算;.NET 5 中仅作软限制,几乎无效 - 别忽略
CacheEntryOptions.SlidingExpiration和AbsoluteExpiration的叠加效果——同时设两者时,任意一个到期即失效,但Compact()仍以绝对时间为判断依据
复杂点在于:清理不是“执行一次就完事”,它和缓存键设计、过期策略、GC 周期、宿主生命周期都咬合在一起。最容易被忽略的是——没在服务停止前做最后一次 Compact(1.0),导致进程退出时缓存还占着内存,被监控误报为泄漏。


















