You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Caffeine缓存导致频繁Major GC 不调整堆和缓存大小的解决方案

问题背景

在项目中使用 caffeine cache 实现缓存能力,配置如下:

cache = Caffeine.newBuilder()
                .expireAfterWrite(6, TimeUnit.MINUTES)
                .maximumSize(50_0000.get())
                .recordStats()
                .build();

当前缓存内存占用约600MB。
heap

缓存读取逻辑如下:

ReadOnlyHashTable v = itemPropsCache.getIfPresent(key);

若读取未命中,则从Redis加载数据:

// load from redis ...
ReadOnlyHashTable table = new ReadOnlyHashTable('${redisVlue}');
itemPropsCache.setCache(key, table);

经GC日志排查与JVM堆转储分析,老年代内存持续增长,最终触发Full GC。
推测原因:由于配置了6分钟写入过期策略,缓存对象在年轻代经过多次GC达到 MaxTenuringThreshold 阈值后会晋升到老年代,每6分钟就会在老年代产生约500MB的待回收垃圾,待对象过期后才会被清理。

已尝试CMS和G1垃圾回收器,均未达到理想的回收效果。

CMS回收器JVM参数配置

-server
-Xmx6g
-Xms6g
-XX:NewRatio=1
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=75
-XX:+UseCMSInitiatingOccupancyOnly
-XX:MaxTenuringThreshold=15
-XX:SurvivorRatio=3
-XX:+ParallelRefProcEnabled
-XX:+CMSParallelRemarkEnabled
-XX:+UseCMSCompactAtFullCollection
-XX:+HeapDumpOnOutOfMemoryError
-XX:MetaspaceSize=512m
-XX:MaxMetaspaceSize=512m
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/export/Logs/gc.log
-XX:+PrintTenuringDistribution

CMS FullGC

G1回收器JVM参数配置

-server
-Xmx6g
-Xms6g
-XX:+UseG1GC
-XX:+HeapDumpOnOutOfMemoryError
-XX:MetaspaceSize=512m
-XX:MaxMetaspaceSize=512m
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/export/Logs/gc.log
-XX:+PrintTenuringDistribution

G1

请问在不调整堆内存大小、不调整缓存大小的前提下,有什么可行的解决方案?


解决方案

按优先级从高到低落地:

  • 补全Caffeine主动过期清理配置
    Caffeine默认的过期条目清理是懒触发的,仅在缓存执行读、写、更新操作时顺带清理关联分片的过期数据,没有访问到的过期条目会一直占用内存无法释放。在构造Cache时追加.scheduler(Scheduler.systemScheduler())配置,会启用JVM内置的单线程调度线程池,默认每秒逐分片轮询清理过期条目,过期对象不会长时间滞留堆内存,绝大多数场景下这个配置就能解决老年代堆积问题。Java 8环境可以自行创建单线程守护的ScheduledExecutorService,传入Scheduler.forScheduledExecutorService()实现同样的定时清理效果。
    额外建议把手动判空+put的加载逻辑替换成Caffeine原生的cache.get(key, k -> { /* 从Redis加载数据并返回ReadOnlyHashTable的逻辑 */ }),原生加载逻辑是原子性的,不会产生重复对象,也能和缓存的清理、统计逻辑完全对齐,避免自定义封装的setCache逻辑残留多余强引用。
  • 调整JVM代际参数,让缓存对象尽量在年轻代完成回收
    现有CMS参数里MaxTenuringThreshold=15设置过高,6分钟生命周期的缓存对象很容易在多次Young GC后晋升到老年代。先统计业务的Young GC间隔,把MaxTenuringThreshold调整到对应值,保证缓存对象从写入到过期的全生命周期都在年轻代,不会晋升到老年代。比如Young GC平均间隔为40秒,把阈值设为8,对象最多在年轻代存活320秒(约5分20秒),还没晋升就已经过期被Young GC回收。
    现有参数里Survivor区单块大小为600MB,刚好能容纳全量缓存的600MB占用,不用担心Survivor放不下导致的提前晋升问题。
  • 优化GC参数,避免老年代回收不及时触发Full GC
    如果继续用CMS,把-XX:CMSInitiatingOccupancyFraction调低到60,老年代占用到60%就启动后台并发标记和清理,不要等内存占比过高才触发,避免老年代预留空间不够容纳浮动垃圾触发Full GC。
    如果用G1,现有参数完全没有做针对性优化,追加以下配置:
    -XX:MaxGCPauseMillis=200
    -XX:G1NewSizePercent=20
    -XX:G1MaxNewSizePercent=40
    -XX:InitiatingHeapOccupancyPercent=35
    -XX:+G1UseAdaptiveIHOP
    -XX:G1MixedGCCountTarget=8
    -XX:G1MixedGCLiveThresholdPercent=80
    
    自适应IHOP会根据历史回收效率提前启动混合回收,把老年代的过期缓存分批次在混合GC阶段回收掉,不会等到堆内存占用过高触发Full GC。
  • 排查多余强引用
    检查自定义封装的itemPropsCache类逻辑,确认没有在Caffeine本身的存储结构之外,用其他List、Map等结构持有缓存值的强引用;检查是否有缓存值被其他业务逻辑的长生命周期对象引用,导致过期后依然无法回收。

内容的提问来源于stack exchange,提问作者Feng Kai

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 08:00:58