线上服务频繁Full GC,排查发现是这个原因
周三下午,监控告警群疯狂报警:生产环境某服务响应时间飙升,Full GC每分钟触发十几次。
排查了两个小时,最后发现是一个很隐蔽的问题。记录一下,希望对大家有帮助。
一、问题现象
监控数据:
- Full GC频率:从正常的几小时一次 → 每分钟10+次
- 服务响应时间:从100ms → 5秒+
- CPU使用率:飙到90%+(大部分是GC线程)
告警信息:
[WARN] GC overhead limit exceeded
[WARN] Full GC (Allocation Failure)
---
## 二、紧急处理
先恢复业务再说。
1. 保留现场
jmap -dump:format=b,file=/tmp/heap.hprof $(pgrep -f myservice)
2. 重启服务
systemctl restart myservice
重启后暂时恢复,但不解决根因迟早还会出问题。
---
## 三、排查过程
### 3.1 先看GC日志
GC日志分析:
2024-01-17T14:32:15.123+0800: [Full GC (Allocation Failure)
[PSYoungGen: 87040K->87040K(87040K)]
[ParOldGen: 174080K->174050K(174080K)]
261120K->261090K(261120K), 0.8523450 secs]
**关键发现:**
- Full GC后内存几乎没释放
- 年轻代和老年代都接近满了
- 说明有大量对象无法回收
### 3.2 分析Heap Dump
用MAT(Memory Analyzer Tool)打开dump文件。
**Leak Suspects Report显示:**
Problem Suspect 1:
One instance of "java.util.concurrent.LinkedBlockingQueue"
occupies 850,000,000 bytes (78% of the heap)
一个LinkedBlockingQueue占了78%的内存?
### 3.3 定位代码
点击查看这个队列的引用链:
java.util.concurrent.LinkedBlockingQueue
↑ java.util.concurrent.ThreadPoolExecutor.workQueue
↑ com.example.service.AsyncService.executor
找到了!是`AsyncService`里的线程池。
### 3.4 查看代码
```java
@Service
public class AsyncService {
// 问题就在这里!
private ExecutorService executor = new ThreadPoolExecutor(
5, // 核心线程数
10, // 最大线程数
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>() // 无界队列!
);
public void asyncProcess(Task task) {
executor.submit(() -> {
processTask(task);
});
}
}
问题根因:
1. 使用了无界队列 LinkedBlockingQueue<>()
2. 任务处理速度 < 任务提交速度
3. 任务不断堆积在队列里
4. 队列越来越大,内存越来越少
5. 最终触发频繁Full GC
四、修复方案
方案1:使用有界队列
private ExecutorService executor = new ThreadPoolExecutor(
5,
20, // 最大线程数调大
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000), // 有界队列,最多1000个任务
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
方案2:推荐配置
@Bean
public ThreadPoolExecutor asyncExecutor() {
return new ThreadPoolExecutor(
10, // 核心线程数
50, // 最大线程数
60L, TimeUnit.SECONDS, // 空闲存活时间
new LinkedBlockingQueue<>(500), // 有界队列
new ThreadFactoryBuilder()
.setNameFormat("async-pool-%d")
.build(),
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
}
阿里Java开发手册明确禁止使用Executors创建线程池!
五、拒绝策略选择
| 策略 | 行为 | 适用场景 |
|---|---|---|
| AbortPolicy | 抛异常 | 不允许丢弃任务 |
| CallerRunsPolicy | 调用者线程执行 | 希望降速,不丢任务 |
| DiscardPolicy | 静默丢弃 | 允许丢弃 |
| DiscardOldestPolicy | 丢弃队列最老的 | 只关心最新任务 |
一般推荐CallerRunsPolicy
六、深入理解:为什么无界队列危险
6.1 ThreadPoolExecutor工作原理
任务提交流程:
1. 线程数 < corePoolSize → 创建新线程
2. 线程数 >= corePoolSize → 放入队列
3. 队列满 && 线程数 < maxPoolSize → 创建新线程
4. 队列满 && 线程数 >= maxPoolSize → 执行拒绝策略
无界队列问题:
- 队列永远不会满
- 步骤3永远不会触发
- maxPoolSize参数失效
- 任务无限堆积 → 内存溢出
6.2 线程池参数估算
CPU密集型:
corePoolSize = CPU核数 + 1
IO密集型:
corePoolSize = CPU核数 * 2 * (1 + 平均等待时间/平均计算时间)
队列容量估算:
queueCapacity = (任务最大响应时间 - 任务平均处理时间) * 每秒任务数
6.3 JVM内存与GC的关系
GC触发条件:
- Young GC:Eden区满
- Full GC:
1. 老年代空间不足
2. Metaspace不足
3. System.gc()调用
4. CMS GC失败(Concurrent Mode Failure)
本次问题:
大量Task对象存活时间长 → 晋升到老年代 → 老年代满 → Full GC
Full GC无法回收(对象还在被引用)→ 反复Full GC → GC overhead
七、如何避免类似问题
7.1 代码规范
// ❌ 禁止
Executors.newFixedThreadPool(10);
new LinkedBlockingQueue<>();
// ✅ 推荐
new ThreadPoolExecutor(
核心线程数,
最大线程数,
存活时间, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(队列容量),
拒绝策略
);
7.2 JVM参数推荐
# 生产环境推荐配置
-Xms4g -Xmx4g # 堆内存,建议相等避免动态调整
-XX:+UseG1GC # G1垃圾收集器
-XX:MaxGCPauseMillis=200 # 目标停顿时间
-XX:+HeapDumpOnOutOfMemoryError # OOM时自动dump
-XX:HeapDumpPath=/tmp/heapdump.hprof # dump路径
-XX:+PrintGCDetails # GC详情
-Xloggc:/var/log/gc.log # GC日志
7.4 监控线程池
@Scheduled(fixedRate = 60000)
public void monitorThreadPool() {
ThreadPoolExecutor executor = asyncExecutor;
log.info("线程池状态: 活跃线程={}, 队列大小={}",
executor.getActiveCount(),
executor.getQueue().size());
if (executor.getQueue().size() > 300) {
// 发送告警
}
}
八、远程Dump分析技巧
生产环境dump文件动辄几个G,下载到本地分析很麻烦。
我用星空组网把本地和服务器连起来,直接scp拉文件:
# 组网后直接传文件
scp root@192.168.188.10:/tmp/heap.hprof ./
或者用VisualVM远程连接JMX实时看内存情况。比开VPN方便,速度也快。
九、总结
这次问题的根因:
无界队列 + 任务处理慢 = 队列无限增长 = 内存溢出 = 频繁Full GC
教训:
- 永远不要用无界队列
- 永远不要用Executors创建线程池
- 线程池必须监控
- 拒绝策略要根据业务选择
希望这篇文章能帮到遇到类似问题的朋友。
有问题评论区交流~

浙公网安备 33010602011771号