找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3243

积分

0

好友

429

主题
发表于 5 小时前 | 查看: 5| 回复: 0

🙋‍♂️ 面试官灵魂拷问:
“线上有遇到过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 怎么处理?

  1. 先止血:jmap -dump 导出堆,保留现场
  2. 重启服务恢复业务
  3. 离线分析堆文件,找大对象
  4. 追代码修复
  5. 上线验证

实际开发建议

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 内存泄漏的坑?欢迎在云栈社区分享你的排查经历和解决方案。




上一篇:Kimi K2.6开源模型快速集成WorkBuddy,前端开发实测体验
下一篇:Qt 5.15 vs Qt6:企业坚守、新手选择与商用授权的深度解析
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-4-23 08:11 , Processed in 1.183316 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表