我在某电商大促场景踩了Full GC的坑,排查了6小时,终于搞定OOM频繁重启问题
我在某电商大促前夕遇到Full GC频繁的坑,系统CPU飙到100%导致服务重启,排查了整整6小时,终于通过更换缓存框架和优化JVM参数使系统恢复稳定,本文给你完整复现步骤。
故障场景还原
业务背景
某电商秒杀系统,大促前夕进行流量预热,日均10万订单,大促期间高峰QPS预计达到500。系统采用Spring Boot 2.7,部署在4核8G服务器上,主要承接订单创建和查询业务。
运行环境
- JDK版本: JDK 17 (LTS)
- Spring Boot: 2.7.14
- Web容器: Tomcat 9.0.n
- 服务器配置: 4核CPU / 8GB内存
- JVM参数 :
-Xms512m -Xmx512m -XX:+UseG1GC
故障现象
2024-11-11 18:30:00 [ERROR] 监控平台P0告警:Full GC频率异常 > 10次/分钟
css
[18:25:23.456] [GC pause (G1 Humongous Allocation)]
[Parallel Time: 823.456 ms]
[Eden: 96.0M(96.0M)->0.0B(96.0M) Survivors: 64.0M->64.0M Heap: 512.0M(512.0M)->512.0M(512.0M)]
[ClassLoader: ~]
系统指标异常:
- Full GC频率: 12次/分钟 (正常: <1次/分钟)
- Full GC耗时: 平均800ms (正常: <200ms)
- 老年代使用率: 95% (正常: <70%)
业务影响:
- 订单创建接口P99延迟: 50ms → 3200ms
- 订单创建成功率: 99.5% → 87%
- 服务重启次数: 3次 (最近1小时)
复现步骤
- 启动应用,配置JVM参数:
bash
-Xms512m -Xmx512m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:InitiatingHeapOccupancyPercent=45
- 使用wrk模拟500 QPS并发请求,持续10分钟:
bash
wrk -t4 -c100 -d600s http://localhost:8080/api/order/create
- 观察服务器内存使用率迅速飙升至95%
- 应用日志出现大量"Full GC"和"OutOfMemoryError"警告
排查链路全记录
【阶段1】问题发现 (18:30)
工具: Prometheus + Grafana
发现:
- CPU使用率100%,线程池活跃线程数=最大线程数
- Full GC频率异常,老年代使用率95%
- 服务重启3次,严重影响业务
初步判断: 可能是内存泄漏或堆内存不足
【阶段2】根因分析 (18:45)
工具和操作:
bash
# 查看GC日志
grep 'Full GC' gc-problem.log | tail -20
# 实时监控
jstat -gcutil <pid> 5000 10
# 导出堆转储
jmap -dump:live,format=b,file=heap.hprof <pid>
发现:
- 线程转储显示大量线程BLOCKED在GC阶段
- MAT分析发现OrderCache占用75%堆内存(约360MB)
- OrderCache使用ConcurrentHashMap,对象数量380万+
代码审查发现:
java
@Component
public class OrderCache {
private static final ConcurrentHashMap<String, Order> cache =
new ConcurrentHashMap<>(); // ❌ 无界缓存,永不清理
public void putOrder(String orderId, Order order) {
cache.put(orderId, order); // ❌ 无过期时间,无限增长
}
}
根本原因:
- 直接原因: OrderCache无限增长,占用75%堆内存
- 深层原因: 使用ConcurrentHashMap做缓存,无淘汰机制
- 设计缺陷: 堆内存512MB太小,G1 GC参数未优化
【阶段3】临时措施 (19:00)
措施1: 扩容
- 操作: 调整JVM参数重启
-Xms512m → -Xms2g - 效果: 堆内存扩大4倍,老年代有更多空间
措施2: 清理缓存
- 操作: 调用cache.clear()接口
- 效果: OrderCache从380万对象 → 0,老年代使用率95% → 35%
措施3: 限流
- 操作: Nginx限流300 QPS
- 效果: 保护下游服务,降低对象创建速率
措施4: 优化GC参数
- 操作:
-XX:InitiatingHeapOccupancyPercent=45 → 30 - 效果: 提前触发Mixed GC,避免Full GC
指标改善:
- Full GC频率: 12次/分钟 → 0次/分钟
- 订单创建P99延迟: 3200ms → 120ms
- 订单创建成功率: 87% → 99.2%
【阶段4】解决方案 (次日10:00)
代码修改:
❌ 旧代码:
java
private static final ConcurrentHashMap<String, Order> cache =
new ConcurrentHashMap<>();
✅ 新代码 (使用Caffeine):
java
@Component
public class OrderCache {
private final Cache<String, Order> cache;
public OrderCache() {
this.cache = Caffeine.newBuilder()
.maximumSize(10000) // 最大1万条
.expireAfterWrite(10, TimeUnit.MINUTES) // 10分钟过期
.recordStats() // 开启统计
.build();
}
}
JVM参数优化:
❌ 旧配置:
ini
-Xms512m -Xmx512m
-XX:InitiatingHeapOccupancyPercent=45
-XX:MaxGCPauseMillis=200
✅ 新配置:
ini
-Xms2g -Xmx2g (堆内存扩大4倍)
-XX:InitiatingHeapOccupancyPercent=30 (提前触发Mixed GC)
-XX:G1HeapRegionSize=16m (优化Region大小)
-XX:MaxGCPauseMillis=100 (更低的停顿目标)
【阶段5】预防措施 (次日14:00)
流程规范:
- 代码审查: 禁止使用无界容器做缓存
- CI检查: 静态分析检测无界集合使用
- 监控告警: GC频率、老年代使用率
- 容量规划: 大促前压测验证
- 应急预案: 降级、限流、扩容方案
长期优化:
- 建立缓存使用规范文档
- 定期故障演练 (每季度)
- 知识分享和培训
错误解法VS正确解法
错误解法1: 只扩容不优化代码
踩坑记录: 我发现Full GC频繁后,第一反应是"内存不够用,加内存!" 于是把堆内存从512MB加到2GB。
结果:
- Full GC频率: 12次/分钟 → 8次/分钟 (有改善,但未根治)
- 2小时后,老年代使用率再次上升到90%
- 问题依然存在,只是延缓了爆发时间
为什么错: 只扩容不解决根本问题,缓存对象仍在无限增长,迟早会撑爆更大的堆。
教训: 必须找到内存增长的根本原因!
正确解法: 双管齐下
方法论: Full GC排查三步法
diff
第1步: 快速止损
- 扩容: 增加堆内存
- 清理: 手动清空缓存
- 限流: 降低请求速率
第2步: 根因分析
- GC日志: 确认GC类型和频率
- jstat: 实时监控内存使用
- MAT: 分析堆转储,找出内存大户
- 代码审查: 定位问题代码
第3步: 彻底解决
- 代码优化: 使用成熟缓存框架
- JVM调优: 优化G1 GC参数
- 监控告警: 建立完善的监控体系
最终配置:
java
// 使用Caffeine缓存
Cache<String, Order> cache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.recordStats()
.build();
bash
# JVM参数
-Xms2g -Xmx2g
-XX:MetaspaceSize=512m
-XX:+UseG1GC
-XX:MaxGCPauseMillis=100
-XX:InitiatingHeapOccupancyPercent=30
-XX:G1HeapRegionSize=16m
完整可运行代码: (见code目录)
性能验证结果
压测环境
makefile
硬件: 4核CPU / 8GB内存 / SSD硬盘
软件: JDK 17 / Spring Boot 2.7.14
JVM参数: 见上文新配置
压测工具和命令
bash
# 使用wrk进行压测
wrk -t4 -c100 -d1800s http://localhost:8080/api/order/create
# 或使用JMH基准测试
java -jar benchmarks.jar
调优前后对比数据
| 指标 | 调优前 | 调优后 | 提升幅度 |
|---|---|---|---|
| Full GC频率 | 12次/分钟 | 0次/分钟 | -100% |
| Young GC频率 | 150次/分钟 | 30次/分钟 | -80% |
| Young GC耗时 | 45ms | 15ms | -67% |
| 老年代使用率 | 95% | 45% | -53% |
| 缓存对象数 | 380万个 | <1万个 | -99.7% |
| 订单创建P99延迟 | 3200ms | 35ms | -99% |
| 订单创建成功率 | 87% | 99.8% | +14.7% |
结论:
通过更换缓存框架和优化JVM参数,系统彻底解决Full GC频繁问题,订单创建成功率提升到99.8%,P99延迟降低99%,大促顺利进行。
核心避坑总结
3条可直接抄的避坑指南:
markdown
❌ 避坑1: 不要用ConcurrentHashMap做缓存
原因: 无界容器,对象无限增长导致OOM
✅ 正确做法: 使用Caffeine或Guava Cache,设置容量和过期
❌ 避坑2: 不要只扩容不优化代码
原因: 治标不治本,迟早还会出问题
✅ 正确做法:
- 找到内存增长的根本原因
- 优化代码逻辑和缓存策略
- 扩容只是辅助手段
❌ 避坑3: 不要忽略JVM参数调优
原因: 默认参数不适应所有场景
✅ 正确做法:
- G1 GC: IHOP=30%, RegionSize=16MB
- 堆内存: 根据QPS和对象大小计算
- 监控: GC频率、老年代使用率
长期架构优化建议:
markdown
1. 代码层面:
- 使用成熟缓存框架 (Caffeine > Guava > ConcurrentHashMap)
- 设置合理的容量上限和过期时间
- 添加缓存统计和监控
- 动态调整JVM参数 (结合配置中心)
2. 监控层面:
- 接入Prometheus采集JVM指标
- 设置Grafana看板实时监控
- 设置告警: Full GC频率>1次/分钟、老年代使用率>80%
3. 容灾层面:
- 配置限流规则 (Nginx/Sentinel)
- 准备降级方案 (关闭非核心功能)
- 大促前压测验证,临时扩容