线上 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+ 小伙伴加入学习,欢迎点击围观

面试考察点

  1. GC 基础功底:面试官想确认你是否清楚 Full GC 的触发条件、各种 GC 算法的适用场景,而不是只会说"Full GC 很慢,要避免"。

  2. 排查思路是否系统化:考察你能否按照 "看现象 → 定原因 → 找根因 → 验证修复" 这套流程来排查,而不是上来就瞎调参数。

  3. 工具熟练度jstatjmapjstackMATArthas 这些工具你用不用得起来?用过和背过,面试官一问就知道。

核心答案

先说排查思路,记住这个 四步法

步骤 做什么 工具
1. 确认 Full GC 是否真的频繁 看 GC 日志、监控指标 jstat、GC 日志、Prometheus + Grafana
2. 判断是什么触发了 Full GC 分析触发原因 GC 日志参数、jstat -gccause
3. 导出堆快照,分析内存占用 找出大对象和泄漏点 jmapMATArthas
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 failureCMS 空间不足

第三步:导出堆快照,用 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

面试高频追问

  1. 追问一:Full GC 和 Young GC 的区别是什么?

    Young GC(Minor GC)只回收新生代(Eden + S0/S1),速度快、停顿短(通常几毫秒到几十毫秒)。Full GC 回收整个堆(新生代 + 老年代)加上元空间,速度慢、停顿长(可能几百毫秒到几秒)。生产环境要尽量减少 Full GC 的频率。

  2. 追问二:怎么判断是内存泄漏还是内存不够用?

    关键看 Full GC 后老年代的使用率:

    • Full GC 后老年代使用率降下来了 → 不是泄漏,是内存确实不够用,加内存或优化对象生命周期
    • Full GC 后老年代使用率没怎么降 → 大概率内存泄漏,对象被 GC Roots 引用链死死拽住,回收不掉
  3. 追问三: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。

  4. 追问四:线上 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 找到内存中的大对象或泄漏点,最后针对性修复。面试时把这套流程讲清楚,再配合一两个真实案例,基本能拿下这道题。