线上 Full GC 频繁,如何排查解决?
一则或许对你有用的小广告
欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书
《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于
Spring AI + Spring Boot 3.x + JDK 21...,查看介绍《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...,查看介绍;演示链接:http://116.62.199.48:7070/《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/
新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍
截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观
面试考察点
-
GC 基础功底:面试官想确认你是否清楚 Full GC 的触发条件、各种 GC 算法的适用场景,而不是只会说"Full GC 很慢,要避免"。
-
排查思路是否系统化:考察你能否按照 "看现象 → 定原因 → 找根因 → 验证修复" 这套流程来排查,而不是上来就瞎调参数。
-
工具熟练度:
jstat、jmap、jstack、MAT、Arthas这些工具你用不用得起来?用过和背过,面试官一问就知道。
核心答案
先说排查思路,记住这个 四步法:
| 步骤 | 做什么 | 工具 |
|---|---|---|
| 1. 确认 Full GC 是否真的频繁 | 看 GC 日志、监控指标 | jstat、GC 日志、Prometheus + Grafana |
| 2. 判断是什么触发了 Full GC | 分析触发原因 | GC 日志参数、jstat -gccause |
| 3. 导出堆快照,分析内存占用 | 找出大对象和泄漏点 | jmap、MAT、Arthas |
| 4. 针对性修复 | 改代码 or 调参数 | 根因不同,方案不同 |
深度解析
一、Full GC 的触发条件
先搞清楚 Full GC 是被什么触发的,不同原因排查方向完全不同:
上图列出了 5 种常见的 Full GC 触发原因。排查时需要先定位到底是哪一种,然后对症下药:
- 老年代空间不足是最常见的原因,通常伴随着内存泄漏或大对象分配
- 元空间不足在大量使用动态代理、反射的框架(如 Spring、MyBatis)中比较常见
System.gc()这个坑,很多 NIO 框架和 RMI 会偷偷调用,可以用-XX:+DisableExplicitGC禁掉- CMS Concurrent Mode Failure 是 CMS 收集器特有的问题,本质是老年代碎片太多或并发标记期间空间不够
二、排查实战——四步走
第一步:确认 Full GC 频率和耗时
# 查看 GC 统计信息,每 1 秒打印一次,共 10 次
jstat -gcutil <pid> 1000 10
# 输出示例:
# S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
# 0.00 45.23 67.89 89.12 95.34 91.22 156 2.345 23 4.567 6.912
# ^^ ^^^^^
# Full GC 次数 耗时
重点关注 FGC(Full GC 次数)和 FGCT(Full GC 总耗时)。如果 FGC 每分钟好几次,FGCT 累积耗时很长,那就是确认 Full GC 频繁了。
同时看 O(老年代使用率),如果老年代一直居高不下(比如 > 85%),说明老年代对象回收不掉,大概率内存泄漏。
# 看 GC 原因
jstat -gccause <pid> 1000 5
# 输出中的 LGCC(上次 GC 原因)和 GCC(当前 GC 原因)能看到触发原因
第二步:看 GC 日志,分析触发模式
线上一定要开启 GC 日志,这是排查 GC 问题的"黑匣子":
# JDK 8 GC 日志参数
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/var/log/gc.log
# JDK 11+ GC 日志参数(格式变了)
-Xlog:gc*:file=/var/log/gc.log:time,uptime:filecount=5,filesize=100M
GC 日志里能看出很多东西:
- 如果 Full GC 前总有一次 Young GC,而且 Young GC 后老年代使用率飙高 → Young GC 晋升对象太多
- 如果 Full GC 后老年代使用率没降多少 → 内存泄漏,对象回收不掉
- 如果日志里有
System字样 →System.gc()触发的 - 如果日志里有
CMS相关的concurrent mode failure→ CMS 空间不足
第三步:导出堆快照,用 MAT 分析
这一步是找到根因的关键。
# 先用一个 jmap 看一下堆中对象统计(影响较小)
jmap -histo <pid> | head -20
# 输出示例:
# num #instances #bytes class name
# 1 1234567 234567890 [B (byte数组)
# 2 567890 89012345 java.lang.String
# 3 345678 56789012 java.util.HashMap$Node
jmap -histo 能快速看到哪些类的实例数最多、占用内存最大。如果发现某个业务类的实例数异常多,基本就锁定方向了。
# 导出完整的堆 dump(注意:这个操作会 STW,线上慎用!)
jmap -dump:format=b,file=heap.hprof <pid>
# 更安全的方式:在 JVM 启动参数中配置 OOM 时自动 dump
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/heapdump.hprof
拿到 .hprof 文件后,用 MAT(Memory Analyzer Tool) 分析:
- Dominator Tree(支配树):看哪个对象占的内存最大
- Leak Suspects(泄漏嫌疑):MAT 自动分析可能的泄漏点
- GC Roots 引用链:找到对象为什么回收不掉(谁引用了它)
第四步:针对性修复
找到根因后,修复方案就比较明确了。下面按常见原因逐个说。
三、常见原因及解决方案
原因 1:内存泄漏——对象回收不掉
这是最常见的坑。典型场景:
// 典型内存泄漏:静态 Map 不断 put,从来不 remove
public class CacheManager {
private static final Map<String, Object> cache = new HashMap<>();
public static void put(String key, Object value) {
cache.put(key, value); // 只进不出,老年代越塞越满
}
}
// 典型内存泄漏:ThreadLocal 用完不 remove
public class UserController {
private static final ThreadLocal<UserContext> threadLocal = new ThreadLocal<>();
public void handle(Request req) {
threadLocal.set(new UserContext(req));
// 处理业务...
// 忘了 remove!线程池复用线程,ThreadLocal 对象一直被引用
}
}
// 典型内存泄漏:未关闭的资源
public void readFiles() {
while (true) {
InputStream is = new FileInputStream("bigfile.dat");
// 忘了 close(),FileInputStream 内部的 Finalizer 对象堆积在老年代
}
}
修复方案:
- 静态缓存要设置过期策略,用
WeakHashMap或 Caffeine/Guava Cache ThreadLocal用完必须remove(),最好放在finally块里- 资源用
try-with-resources自动关闭
原因 2:大对象直接进老年代
// 每次请求都创建一个大数组
public byte[] process() {
byte[] data = new byte[10 * 1024 * 1024]; // 10MB 的大对象
// 超过 PretenureSizeThreshold,直接进老年代
return data;
}
修复方案:
- 避免频繁创建大对象,考虑复用或流式处理
- 调大新生代大小,让部分大对象能在 Young GC 时被回收
原因 3:元空间不足
# 调大 Metaspace 初始值和最大值
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m
# 注意:要把初始值和最大值设成一样,避免元空间反复扩容触发 Full GC
原因 4:System.gc() 被框架偷偷调用
# 禁掉显式 GC 调用(慎用,NIO 的 DirectByteBuffer 回收可能受影响)
-XX:+DisableExplicitGC
# 更好的方案:限制显式 GC 的行为而非完全禁用(JDK 7u4+)
-XX:+ExplicitGCInvokesConcurrent
四、JVM 参数调优速查表
| 场景 | 推荐参数 | 说明 |
|---|---|---|
| 通用配置 | -Xms4g -Xmx4g |
堆大小固定,避免动态扩缩 |
| 元空间 | -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m |
初始值 = 最大值,避免扩容 |
| GC 日志 | -Xlog:gc*:file=gc.log |
必须开,排查问题的 "黑匣子" |
| OOM 自动 dump | -XX:+HeapDumpOnOutOfMemoryError |
OOM 时自动导出堆快照 |
| 禁显式 GC | -XX:+DisableExplicitGC |
防止框架偷调 System.gc() |
五、排查工具速查
| 工具 | 用途 | 备注 |
|---|---|---|
jstat -gcutil |
实时看 GC 频率和各区域使用率 | 轻量,线上首选 |
jstat -gccause |
看 GC 触发原因 | 定位触发类型 |
jmap -histo |
看堆中对象统计 | 轻量,影响小 |
jmap -dump |
导出堆快照 | 较重,会 STW |
jstack |
看线程栈 | 排查是否有线程阻塞或死锁 |
| MAT | 分析 .hprof 堆快照 |
找泄漏嫌疑点的利器 |
| Arthas | 在线诊断,不用重启应用 | 阿里开源,生产神器 |
| GCViewer | 分析 GC 日志可视化 | 看 GC 趋势和模式 |
六、Arthas 在线排查实战
线上排查 Full GC,Arthas 是真的好用,不用重启应用就能诊断:
# 安装并启动 Arthas
java -jar arthas-boot.jar
# 1. 监控方法调用,看是否有异常频繁的调用
watch com.example.Service process '{params, returnObj}' -n 10
# 2. 查看某个类的实例数量和占用内存
heapdump --live /tmp/dump.hprof # 导出 live 对象的 dump
# 3. 监控内存使用趋势
memory # 查看 JVM 各区域内存使用情况
# 4. 查看线程状态,找是否有线程阻塞
thread -n 5 # 显示 CPU 占用最高的 5 个线程
# 5. 反编译类,看线上跑的代码是否是最新的
jad com.example.CacheManager
面试高频追问
-
追问一:Full GC 和 Young GC 的区别是什么?
Young GC(Minor GC)只回收新生代(Eden + S0/S1),速度快、停顿短(通常几毫秒到几十毫秒)。Full GC 回收整个堆(新生代 + 老年代)加上元空间,速度慢、停顿长(可能几百毫秒到几秒)。生产环境要尽量减少 Full GC 的频率。
-
追问二:怎么判断是内存泄漏还是内存不够用?
关键看 Full GC 后老年代的使用率:
- Full GC 后老年代使用率降下来了 → 不是泄漏,是内存确实不够用,加内存或优化对象生命周期
- Full GC 后老年代使用率没怎么降 → 大概率内存泄漏,对象被 GC Roots 引用链死死拽住,回收不掉
-
追问三:CMS 和 G1 在 Full GC 上的表现有什么区别?
CMS 的 Full GC 退化为 Serial Old,单线程回收,停顿时间很长。G1 正常情况下不会触发真正的 Full GC,它通过 Mixed GC(同时回收新生代和部分老年代 Region)来避免全堆回收。如果 G1 真的触发了 Full GC,说明问题已经很严重了(
-XX:InitiatingHeapOccupancyPercent阈值被突破),而且 G1 的 Full GC 在 JDK 10 之前也是单线程的,JDK 10 之后才支持并行 Full GC。 -
追问四:线上
jmap -dump导致服务不可用怎么办?jmap -dump会触发 STW(Stop-The-World),堆越大停顿越长。几个建议:- 优先用
-XX:+HeapDumpOnOutOfMemoryError让 JVM 自己在 OOM 时 dump - 如果必须手动 dump,从流量入口摘掉该节点(从负载均衡中移除),dump 完再加回来
- 用
gcore先生成 core dump,再离线用jmap从 core dump 中提取 heap dump
- 优先用
常见面试变体
- "线上接口突然变慢,怎么排查?和 GC 有什么关系?"
- "说说你遇到过的一次线上 GC 问题排查过程"
- "如何减少 Full GC 的频率?"
- "什么情况下 Full GC 后老年代使用率还是很高?"
记忆口诀
排查四步走:看频率(jstat)→ 找原因(GC 日志)→ 导快照(jmap)→ MAT 分析。五个触发点:老年代满、元空间满、System.gc()、晋升失败、CMS 退化。
总结
线上 Full GC 频繁的排查,核心就一句话:先用 jstat 和 GC 日志定位触发原因,再用 jmap + MAT 找到内存中的大对象或泄漏点,最后针对性修复。面试时把这套流程讲清楚,再配合一两个真实案例,基本能拿下这道题。
