
Nacos 迁移引发 Full GC 问题深度分析文档背景搜索服务 将配置从本地application.yaml迁移到 Nacos 后生产环境运行一段时间后出现 Full GC 频繁CPU 和内存飙升接口 P99 超时严重。本文档结合生产 GC 日志-search-prod-sbwj9_gc.log与代码 Review 进行深度根因分析并给出解决方案。一、问题现象1.1 生产故障时间线来自 GC 日志时间事件2026-06-03 16:23服务启动JDK 21 G1GC堆 8GB 固定Old 区仅 1 region4MB16:23 ~ 17:20Young GC 正常Old 区快速增长至 309 regions1236MB22:17首次 Evacuation Failure堆使用 8033MGC 无法移动对象23:05 ~ 00:58EF 持续出现约每 30 分钟一次Old 区累计增长至 653 regions01:12严重 Evacuation Failure单次 STW 暂停 328ms2026-06-04 08:35Old 区增至 890 regions3560MB占堆 43%14:45Old 区急剧增至 1699 regions6796MB占堆 83%16:15连环 Evacuation Failure 爆发见下方详情16:17:16触发 Full GCG1 Compaction PauseSTW 暂停 6.23 秒1.2 Full GC 爆发前的连环 Evacuation Failure16:15~16:17GC(1472) Evacuation Failure 8151M-7748M 18ms GC(1475) Evacuation Failure 8190M-7884M 23ms GC(1476) Evacuation Failure 8180M-8144M 12ms ← 几乎没回收 GC(1477) Evacuation Failure 8180M-8138M 6ms ← 堆几乎全满 ...连续多次 EF约10次... GC(1494) Evacuation Failure 8187M-8187M 7ms ← 完全没回收 GC(1495) Pause Full (G1 Compaction Pause) 8187M-7475M(8192M) 6230.739msFull GC 停顿 6.2 秒所有业务线程完全暂停是 P99 接口超时的直接原因。Full GC 后依然有 7475M 存活对象说明 Old 区对象完全无法释放——这是内存泄漏的铁证。1.3 Old 区单调增长趋势内存泄漏特征16:23启动 Old: 1 regions ( 4 MB) 17:20 Old: 309 regions ( 1236 MB) ← 1小时涨1232MB 22:23 Old: 653 regions ( 2612 MB) 03:16次日 Old: 721 regions ( 2884 MB) 08:35 Old: 890 regions ( 3560 MB) 14:45 Old: 1699 regions ( 6796 MB) ← 严重 16:17 Old: 1878 regions ( 7512 MB) ← 接近上限2048 regions 8192MBMixed GC 每次仅能回收 1~2 个 Old region说明 Old 区对象大量存活、无法被回收。这是内存泄漏而非正常晋升。1.4 Humongous 大对象问题GC 日志中频繁出现G1 Humongous Allocation触发 Concurrent StartHumongous regions 峰值达 110 regions440MB。Humongous 对象2MB直接分配到 Old 区加速 Old 区填满。二、NacosValue autoRefreshed true的原理2.1 Nacos SDK 的注入机制NacosValue(autoRefreshed true)由 Nacos Spring 扩展nacos-spring-context实现核心原理Nacos 配置中心 │ ▼ 配置变更推送 NacosValueAnnotationBeanPostProcessorBeanPostProcessor │ ─── 在每个 Bean 初始化后扫描其 NacosValue 字段 │ ─── 将 bean 实例引用注册到内部 Map 中 │ ▼ 配置刷新时 遍历 Map通过反射将新值写入所有注册的 bean 字段2.2 关键数据结构Nacos SDK 源码// NacosValueAnnotationBeanPostProcessor 内部 MapString, ListNacosValueTarget placeholderNacosValueTargetMap; // key: ${browser..result-rank.url} // value: List{ Object beanInstance强引用, Field field }2.3 Prototype Bean 注册时机与问题根源Spring 每次创建 prototype bean 时实例化 → 依赖注入 →BeanPostProcessor.postProcessAfterInitialization()NacosValueAnnotationBeanPostProcessor扫描NacosValue字段将 bean 实例强引用追加到placeholderNacosValueTargetMapPrototype bean 生命周期结束Spring 不回调销毁方法也不从 Nacos 注册表移除结果每一个 prototype bean 实例都被 Nacos 的 Map 强引用永远无法被 GC。三、内存泄漏根因分析结合 GC 日志数据验证3.1 泄漏路径每次 HTTP 请求到达 SearchController.search() │ ▼ SearchService.search() ├── applicationContext.getBean(SearchReq) ← prototype bean 创建 ├── applicationContext.getBean(InnerRequest.class) ← prototype bean 创建 └── graph.run() → DAG 执行以下 prototype beans ├── SearchThreeOptimizeRank ← NacosValue rerankUrl ├── VideoTitleVectorRecallBuilder ← NacosValue 2个字段 ├── NewsTitleVectorRecallBuilder ← NacosValue 2个字段 ├── HotTitleVectorRecallBuilder ← NacosValue 2个字段 ├── VideoI2iVectorRecallBuilder ← NacosValue 5个字段 ├── NewsI2iVectorRecallBuilder ← NacosValue 5个字段 ├── SearchAbstractRecallBuilder ← NacosValue 2个字段 └── ~30 其他 RecallBuilder/Recall 类 ← 各含 NacosValue 字段 每个 NacosValue 字段 → Nacos 注册表追加一条强引用 请求结束 → bean 无法被 GCNacos Map 持有强引用3.2 GC 日志数据验证泄漏速率时段Old 区增量时长泄漏速率16:23 → 17:201232 MB57 min~21 MB/min17:20 → 22:231376 MB303 min~4.5 MB/min22:23 → 08:35948 MB612 min~1.5 MB/min08:35 → 14:453236 MB370 min~8.7 MB/min流量高峰启动初期泄漏速率最高21 MB/min与服务刚上线流量集中、每次请求创建大量 prototype bean 完全吻合。夜间低峰泄漏速率降低白天高峰再次飙升。3.3 Mixed GC 无效的原因G1 的 Mixed GC 用于回收 Old 区存活率低的 region。正常服务中Old 区对象会随请求完成而死亡Mixed GC 能有效回收。本案例中Old 区 prototype bean 实例被 Nacos 强引用对象全部存活Mixed GC 扫描后发现无可回收对象回收量趋近于 0无法阻止 Old 区持续增长。3.4 受影响的 Prototype Bean 完整清单以下类均存在Scope(PROTOTYPE) NacosValue(autoRefreshedtrue)组合每次请求均会泄漏类名NacosValue 字段数影响等级SearchThreeOptimizeRank1rerankUrl高VideoI2iVectorRecallBuilder5ragTimeout, k, numCandidates, similarity, embeddingTimeout高NewsI2iVectorRecallBuilder5同上高VideoTitleVectorRecallBuilder2vectorStageTimeout, ragTimeout高NewsTitleVectorRecallBuilder2同上高HotTitleVectorRecallBuilder2同上高SearchAbstractRecallBuilder2ragTimeout, browserTimeout高IntentionResultLlmRecallBuilder3url, model, apikey中IntentionDomainLlmRecallBuilder3url, model, apikey中AiSearchDomainIntentionLlmRecallBuilder3url, model, apikey中QueryListRecallBuilder2sugUrl, vectorStageTimeout中SugGameRecallBuilder等 30 个类各 1~2 个中3.5 为什么迁移前没有问题对比项迁移前Value迁移后NacosValue autoRefreshedtrue注入时机Bean 创建时读一次之后不维护引用注册 bean 引用到全局 Map持续持有prototype bean 能否被 GC✅ 可以无外部强引用❌ 不能Nacos Map 持有强引用配置动态刷新❌ 不支持✅ 支持内存泄漏无有每请求泄漏四、Humongous 对象分析GC 日志中频繁出现Pause Young (Concurrent Start) (G1 Humongous Allocation)说明有 2MB 的大对象频繁分配触发 Concurrent Mark。疑似来源结合代码向量数据titleVector_simbert、titleVector_bgelarge等 Embedding 向量float 列表1024 维 × 4 bytes 4KB不构成 Humongous召回结果集多路召回合并后的ListItemInfo每个 ItemInfo 含完整 VO 对象含向量字段多路汇总后可能超过 2MBJSON 序列化GSON.toJson(itemListToCache)序列化大结果集写入 Redis产生大 StringCompletableFuture 链DAG 并行执行时多个 future 链持有中间结果对象Humongous 对象直接分配到 Old 区与内存泄漏叠加双重加速 Old 区填满。五、分析结论主因确认NacosValue(autoRefreshed true)Scope(PROTOTYPE)组合导致系统性内存泄漏。GC 日志中 Old 区从启动到崩溃 24 小时单调递增、Mixed GC 无效回收是该结论的直接证据。故障链路1. prototype bean 创建 → Nacos 强引用注册 2. → Old 区持续增长泄漏速率 ~1.5~21 MB/min 3. → Mixed GC 无效对象全部存活 4. → Old 区占堆 90%7512MB/8192MB 5. → Evacuation Failure 连环触发 6. → Full GCG1 Compaction PauseSTW 6.23秒 7. → 业务线程全部暂停 8. → P99 接口大量超时受影响范围50 个 prototype 类其中 7 个核心 RecallBuilder/Rank 类单次请求共泄漏约 19 个NacosValue注册条目。触发时间规律启动后约 6~8 小时 Old 区达到 50% 阈值开始触发 Concurrent Mark约 12~18 小时开始出现 Evacuation Failure约 24 小时触发 Full GC。流量越大爆发越快。六、解决方案方案一推荐将 NacosValue 收敛到 Singleton 配置类核心思路prototype bean 不直接持有NacosValue字段改为注入 singleton 配置类获取值。singleton 只注册一次彻底消除泄漏动态刷新能力完全保留。Step 1新建统一配置持有类Slf4j Component // 默认 singleton全局只注册一次到 Nacos public class SearchNacosConfig { // 原 SearchThreeOptimizeRank 中的字段 NacosValue(value ${browser..result-rank.url}, autoRefreshed true) private String rerankUrl; // 原各 RecallBuilder 中的超时字段 NacosValue(value ${.search.timeout.vector-stage:250}, autoRefreshed true) private long vectorStageTimeoutMs; NacosValue(value ${elasticsearch.rag-cluster-query-timeout-ms:1000}, autoRefreshed true) private long ragClusterQueryTimeoutMs; NacosValue(value ${elasticsearch.browser-cluster-query-timeout-ms:350}, autoRefreshed true) private long browserClusterQueryTimeoutMs; NacosValue(value ${.search.i2i.k:20}, autoRefreshed true) private int i2iK; NacosValue(value ${.search.i2i.num-candidates:50}, autoRefreshed true) private int i2iNumCandidates; NacosValue(value ${.search.i2i.similarity:0.7}, autoRefreshed true) private double i2iSimilarity; NacosValue(value ${.search.i2i.embedding-timeout-ms:200}, autoRefreshed true) private long i2iEmbeddingTimeoutMs; // ... 其余所有 NacosValue 字段 public String getRerankUrl() { return rerankUrl; } public long getVectorStageTimeoutMs() { return vectorStageTimeoutMs; } public long getRagClusterQueryTimeoutMs() { return ragClusterQueryTimeoutMs; } public long getBrowserClusterQueryTimeoutMs() { return browserClusterQueryTimeoutMs; } // ... }Step 2修改 SearchThreeOptimizeRankSlf4j Component(SearchThreeOptimizeRank) Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public class SearchThreeOptimizeRank extends AbstractRank { // ❌ 删除会泄漏 // NacosValue(value ${browser..result-rank.url}, autoRefreshed true) // private String rerankUrl; // ✅ 改为注入 singleton 配置类 Resource private SearchNacosConfig nacosConfig; // 使用时nacosConfig.getRerankUrl() }Step 3对所有受影响的 prototype 类执行同样改造所有含NacosValue的 RecallBuilder / Recall / Rank prototype 类均按此方式改造用Resource SearchNacosConfig nacosConfig替换直接注解字段。优点彻底消除内存泄漏Old 区将恢复正常 GC 回收动态刷新完全保留singleton 字段正常更新所有 prototype 通过方法调用实时读取改动范围清晰逐类迁移不影响业务逻辑方案二辅助针对 Humongous 大对象优化建议同步排查大对象来源减少 Humongous 分配频率// 1. 召回结果集控制上限各路召回已有 limit确认执行到位 // 例如 SearchThreeOptimizeRank.scoreSortMergeResults() 已有 limit(20)确保生效 // 2. Redis 缓存序列化前控制大小 // saveDataToCache 前检查 json 大小超过阈值如 1MB跳过缓存 if (jsonStr.length() 1024 * 1024) { redisUtils.set(...); }# 3. G1 HeapRegionSize 调大提高 Humongous 门槛 # 当前 HeapRegionSize4MBHumongous 门槛 2MB可改为 -XX:G1HeapRegionSize8m # Humongous 门槛提升到 4MB方案三应急缓解重启 JVM 参数调整不能根治仅作紧急止血# 1. 立即重启 pod清空泄漏的 Old 区 # 2. 开启 OOM 时自动 Heap Dump下次复现时捕获现场 -XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/tmp/heap-dump.hprof # 3. 降低 IHOP 阈值让 G1 更早启动 Concurrent Mark争取更多回收时间 -XX:InitiatingHeapOccupancyPercent35 # 当前默认约 45% # 4. 增加并发 GC 线程当前 Concurrent Workers 仅 2CPU 4核可调到 2~3 -XX:ConcGCThreads3七、验证方案7.1 修复前 Heap Dump 分析在下次服务启动后约 2~4 小时Old 区增长到约 500MB 时执行jmap -dump:live,formatb,file/tmp/heap.hprof pid用 Eclipse MAT 打开查看NacosValueAnnotationBeanPostProcessor→placeholderNacosValueTargetMap的 retained heap按 retained heap 排序的 Top 10 对象应能看到大量SearchThreeOptimizeRank、VideoI2iVectorRecallBuilder等 prototype bean 实例堆积7.2 修复后的预期 GC 表现对比当前 GC 日志修复后应观察到指标修复前实测修复后预期Old 区增长趋势24h 从 4MB 增至 7512MB无法回收趋于稳定Mixed GC 可有效回收Evacuation Failure22:17 起持续约 30min 一次消失Full GC运行约 24h 后触发STW 6.23s消失Mixed GC 回收量每次回收 1~2 个 Old region每次回收数十个 Old regionP99 延迟超时严重6s STW恢复正常7.3 压测验证方法修复上线后施加持续 QPS 压测建议与生产峰值 QPS 相同持续 30 分钟观察 Old 区是否保持平稳不超过 20% 堆大小确认无 Evacuation Failure、无 Full GC八、优先级与实施计划优先级动作说明时间P0 立即重启 pod临时恢复服务清空当前泄漏的 Old 区今日P0 立即加-XX:HeapDumpOnOutOfMemoryErrorJVM 参数下次复现时自动抓取现场今日P0 1天内按方案一改造SearchThreeOptimizeRank及 6 个高频 RecallBuilder覆盖最高频泄漏路径1天内P1 本周全量排查 50 prototype 类完成所有NacosValue迁移彻底消除泄漏本周P1 本周执行压测验证确认 Old 区不再单调增长验证修复有效性本周P2 下周添加 JVM Old Gen 监控告警Old Gen 60% 预警 80% 紧急提前发现类似问题下周P2 下周排查 Humongous 大对象来源优化召回结果集大小控制降低 Old 区分配压力下周