🙋♂️ 面试官灵魂拷问:
“线上有遇到过Full GC频繁吗?怎么排查的?”
😨 求职者内心 OS:
“遇到过...但当时是运维解决的,我不太清楚具体过程...”
故事开场
上周二下午,正在写代码,钉钉突然弹出一条告警:
⚠️ 服务告警:Full GC 频繁,每分钟3次!
我当时就一个激灵,赶紧登上服务器。一顿操作下来,最后发现——问题根源就是一个 Map 往里塞东西,塞太多了,触发了频繁的 Full GC。
具体情况是这样的。
问题定位过程
第一步:收到告警,先看现象
# 先确认服务状态
ps aux | grep java
# 看GC日志
tail -f gc.log
看到日志:
[Full GC (Allocation Failure) 2024-03-26T14:30:12.123+0800:
Heap: 2048M->2047M(2048M), 2.345s]
[Full GC (Allocation Failure) 2024-03-26T14:30:45.456+0800:
Heap: 2048M->2047M(2048M), 2.567s]
[Full GC (Allocation Failure) 2024-03-26T14:31:12.789+0800:
Heap: 2048M->2047M(2048M), 2.789s]
关键信息:
- 每30秒一次 Full GC
- 每次 GC 后堆内存几乎没降(2048M->2047M)
- GC时间越来越长(2.3s → 2.5s → 2.7s)
第二步:导出堆转储,分析大对象
# 触发一次Full GC并导出堆
jmap -dump:format=b,file=heap.hprof <pid>
用 MAT 打开分析,按 Retained Size 倒序排列:
| 对象 |
Retained Size |
数量 |
HashMap<String, Order> |
1.8GB |
1 |
| Order 对象 |
1.5GB |
500万 |
| String 对象 |
300MB |
500万 |
好家伙,一个 HashMap 占用了 1.8GB 内存,里面塞了 500万个 Order 对象。
第三步:追代码,找到根因
顺着引用链往上追,找到这段代码:
@Service
public class OrderService {
// 划重点!这个就是罪魁祸首
private static Map<String, Order> orderCache = new HashMap<>();
public void processOrder(Order order) {
// 每次下单都往里塞,从不清空
orderCache.put(order.getId(), order);
}
public Order getOrder(String orderId) {
return orderCache.get(orderId);
}
}
问题根因:
- Map 只进不出,内存持续增长
- Eden 区满了,对象进入老年代
- 老年代也满了,触发 Full GC
- 但 GC 后对象还在,内存依然不够
- 进入了恶性循环:GC → 对象还在 → 又GC → 对象还在
第四步:紧急修复
方案一:加容量上限(推荐)
private static Map<String, Order> orderCache = new LinkedHashMap<>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > 100000; // 超过10万条自动淘汰最老的
}
};
方案二:用 WeakHashMap(更推荐)
private static Map<String, Order> orderCache = new WeakHashMap<>();
方案三:加定时清理(简单粗暴)
// 用定时任务,每小时清一次
@Scheduled(cron = "0 0 * * * ?")
public void clearCache() {
orderCache.clear();
}
面试必背知识点
Full GC 触发条件
| 触发条件 |
说明 |
| 老年代空间不足 |
对象太多,晋升失败 |
| 元空间不足 |
类加载太多 |
| System.gc() |
手动调用(不建议) |
| CMS 晋升失败 |
担保失败 |
| G1 Mixed GC 紧急情况 |
G1 回收不过来 |
排查 Full GC 的命令
| 命令 |
作用 |
jstat -gcutil <pid> 1000 |
观察GC频率和堆使用 |
jmap -heap <pid> |
查看堆配置和使用 |
jmap -dump:format=b,file=heap.hprof <pid> |
导出堆转储 |
jmap -histo <pid> |
查看对象分布 |
GCViewer / GCEasy |
分析GC日志 |
常见 Full GC 原因
| 原因 |
典型场景 |
解决方案 |
| 内存泄漏 |
Map/Set/List 只塞不清 |
加容量上限/WeakHashMap |
| 大对象 |
一次加载大文件 |
分批处理/流式处理 |
| 反射/动态代理 |
CGLib/JDK代理滥用 |
缓存Class对象 |
| 元空间满 |
频繁Class.forName |
检查ASM/CGLib |
| CMS晋升失败 |
对象太多,升太快 |
加大老年代 |
常见面试追问
Q1:Full GC 和 Minor GC 有什么区别?
- Minor GC(Young GC):发生在年轻代,清理Eden区和Survivor区,停顿时间短,频率高
- Full GC:发生在整个堆(包括年轻代和老年代),清理所有区域,停顿时间长,频率低
Q2:为什么Full GC后内存没降?
说明对象还在被引用,无法回收。可能是:
- 真正的业务对象(正常的)
- 内存泄漏(Map只增不减、静态集合、监听器未清理)
- 缓存没清(WeakHashMap可解决)
Q3:如何判断是内存泄漏还是正常?
连续多次 Full GC 后,堆内存呈上升趋势→内存泄漏。连续多次 Full GC 后,堆内存呈平稳→正常。
Q4:线上 Full GC 怎么处理?
- 先止血:
jmap -dump 导出堆,保留现场
- 重启服务恢复业务
- 离线分析堆文件,找大对象
- 追代码修复
- 上线验证
实际开发建议
1. 永远不要用 static Map/Set/List 做缓存
// 错误的写法
private static Map<String, Object> cache = new HashMap<>();
// 正确的写法
private Map<String, Object> cache = new WeakHashMap<>(); // 推荐
// 或
private Map<String, Object> cache = new LinkedHashMap<>() {
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > 10000;
}
};
2. 监控要跟上
# Prometheus 监控配置
- pattern: jvm_memory_used_bytes{area="heap"}
labels:
service: your-service
alerts:
- name: HighHeapUsage
expr: jvm_memory_used_bytes / jvm_memory_max_bytes > 0.8
severity: warning
3. 启动参数要合理
# 推荐配置(G1)
-XX:+UseG1GC
-Xms2g -Xmx2g
-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=8m
-XX:InitiatingHeapOccupancyPercent=45
总结
Full GC 频繁不用慌,先看日志再 dump。 堆里找个大 Map,容量上限加一加。
顺口溜记一下:
Full GC 频繁不要慌,先看日志别联网。 dump 文件导出来,MAT 里面找大状。 静态集合没设限,加上上限不用慌。 监控告警要跟上,内存泄漏无处藏。
思考题
你们线上有没有踩过 Map 内存泄漏的坑?欢迎在云栈社区分享你的排查经历和解决方案。