第三方强引用缓存(如无容量限制的hashmap)导致运行两周必oom,本质是对象持续堆积且无法被gc回收,属于确定性内存泄漏;需通过jmap、mat定位大map实例,替换为caffeine等带自动驱逐策略的缓存组件,并配置内存水位限流与heapdump监控。

第三方强引用缓存(比如没设容量限制的 HashMap 或 ConcurrentHashMap)导致系统运行两周必 OOM,本质是对象持续堆积、无法被 GC 回收。这不是偶然故障,而是确定性内存泄漏——只要流量不停、写入不止,OOM 就只是时间问题。
定位强引用缓存的具体位置
不能只盯着“用了 HashMap”,要确认它是否:被静态变量持有、作为单例成员长期存活、且写入后从不清理。
- 用
jmap -histo:live <pid></pid>查看实例数量暴增的类,重点关注自定义缓存容器或高频 new 出的对象 - 导出堆转储(
jmap -dump:format=b,file=heap.hprof <pid></pid>),用 MAT 分析 Dominator Tree,找 retained size 最大的 Map 实例 - 检查该 Map 的 key 是否为长生命周期对象(如 Class、Spring Bean、配置类),value 是否含大对象(如 byte[]、List
)
用带淘汰策略的缓存组件替代原始 Map
强引用 + 无上限 = 内存定时炸弹。必须换成能自动驱逐的方案,而非手动加 if 判断清理。
- 优先选用 Caffeine:支持 LRU/LFU、权重限制、expireAfterWrite/TTL、maximumSize 等,API 清晰,性能优于 Guava Cache
- 若必须用 ConcurrentHashMap,至少包装一层淘汰逻辑:用
LinkedHashMap重写removeEldestEntry()实现 LRU,但需注意线程安全,建议直接交给 Caffeine 处理 - 避免用
WeakReference或SoftReference单独包裹 value —— 它们解决不了 key 强引用导致的 Map 本身膨胀问题
设置硬性内存水位与主动限流
即使换了 Caffeine,也要防住突发流量冲垮缓存。靠 JVM 参数兜底不如业务层控制。
- 给缓存设明确上限:
maximumSize(10_000)或maximumWeight(100_000_000)(按字节估算 value 大小) - 在缓存 put 前检查 JVM 堆使用率(通过
MemoryUsage.getUsed() / getMax()),超过 80% 时拒绝写入并打告警日志 - 对高频写缓存的接口增加 QPS 限流(如 Sentinel 或 Resilience4j),防止缓存成为流量放大器
补充 JVM 和监控保障
技术方案要配可观测能力,否则下次还得等两周才暴露。
- 启动参数必须加:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/dump/,确保 OOM 当场留证 - 配置元空间大小:
-XX:MaxMetaspaceSize=256m,避免因缓存 key 是动态生成类(如 Lambda、CGLIB)引发 Metaspace OOM - 在 Prometheus + Grafana 中接入
jvm_memory_used_bytes{area="heap"}和缓存命中率指标,设置「连续 4 小时堆内存上升斜率 > 2MB/min」的告警










