← 每日面试题 学习看板

频繁FullGC问题排查

面试题/01实践场景题/问题排查/频繁FullGC问题排查.md

面试记录 2026-10-22

题目:频繁 FullGC 问题排查

难度:high

标签:practice/debug, jvm/gc, difficulty/high

评分:⭐⭐⭐☆☆(3/5)

我的回答

出现频繁fullgc首先看开始出现的时间,先分析当时发生了什么,另外如果是周期性的当时没能处理好,是否在每个周期个固定的时间发生?

导出fullgc日志,检查回收前后堆空间的变化,如果变化很小,回收了也没有回收多少内容,并且是持续fgc那么高概率是内存泄露了;如果fgc后能回收较多的内容,证明回收能回收掉并不是内存泄露;

导出内存快照,检查其中对象分布,看具体是什么类型的对象占用大量内存,并且导出当前的jstack线程栈,看是什么线程在持续运行,对应的方法是否有内存泄露的内容;

点评

优点:三条主线都抓对了——时间线(何时开始、是否周期性)、GC 日志判泄漏(回收前后对比)、堆快照 + 线程栈定位持有者。尤其是"先看发生时间对应什么变更"这一步,是线上问题最容易被忽略也最有效的一步。

不足:① 判据不够严谨:只看"单次回收比例"会误判,正确判据是Full GC 后的存活对象占用是否逐次抬升(post-GC old gen 的趋势线),单次回收少可能只是老年代碎片或大对象;② 没有 GC cause 的分析——Allocation Failure、System.gc、Metadata GC Threshold、Humongous Allocation、Promotion Failed 各自对应完全不同的根因;③ 工具链不具体:jstat -gcutil、-Xlog:gc*、jcmd GC.heap_info、jmap -histo:live、MAT 的 Dominator Tree / Leak Suspects 都没提;④ 漏了两个高频真因:元空间泄漏(动态代理/CGLIB 类不断生成)和堆外/直接内存(Full GC 也降不下来,因为它根本不在堆里)。

补充:一个非常好用的判据组合——① jstat -gcutil <pid> 1000 看 O(老年代使用率)在每次 FGC 后是否回到同一基线:逐次抬高 = 泄漏,回落但很快又满 = 分配速率过高或堆太小;② 同时看 FGC 次数增速与 FGCT 平均耗时,若单次 FGC 耗时从 200ms 涨到 2s,说明存活对象越来越多,也是泄漏的强信号。

完整讲解

第一部分:知识重点剖析

核心考点定位:考察能否把"频繁 Full GC"拆成三类根因——真泄漏、容量不足/分配过快、非堆问题,并用趋势判据而不是单点快照来区分。

知识点拆解:

  1. 时间线对齐:Full GC 开始时间点对应的发布、配置变更、流量变化、定时任务、缓存预热。周期性出现(如每天 2:00)优先怀疑定时任务、批量导出、报表、缓存全量刷新。
  2. GC 日志采集:JDK 8 用 -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:<file>,JDK 9+ 用 -Xlog:gc*,gc+heap=debug:file=gc.log:time,uptime,level,tags。关键是能拿到每次 GC 前后的各代容量与耗时。
  3. 趋势判据:用 jstat -gcutil <pid> 1000 观察 O 与 FGC。每次 FGC 后老年代基线逐步抬高 = 存活对象在累积 = 泄漏;每次都能回到同一基线但增长极快 = 分配速率高、新生代过小或对象过早晋升;老年代不高但 FGC 频繁 = System.gc() 被调用、元空间不足、Humongous 分配(G1)。
  4. GC cause 分类:Allocation Failure(正常分配失败)、System.gc()(有人显式调用,常见于 NIO DirectByteBuffer 回收或第三方库)、Metadata GC Threshold(元空间不足)、Humongous Allocation(G1 大对象超过 region 一半)、Promotion Failed(晋升失败,Survivor 不足或老年代碎片)。
  5. 非堆也要查:元空间(-XX:MetaspaceSize/MaxMetaspaceSize,类加载器泄漏、动态代理类暴涨)、直接内存(-XX:MaxDirectMemorySize、Netty)、线程栈(线程数暴涨,每线程 1MB 预留)、代码缓存(CodeCache 满会触发编译退化为解释执行,间接影响 GC 节奏)。
  6. 定位持有者:jmap -histo:live <pid> 看类实例数与总字节数 TOP;jcmd <pid> GC.heap_dump 导出后 MAT 看 Dominator Tree 与 Path to GC Roots,确认是静态集合、缓存、ThreadLocal、监听器还是连接池持有。配合 jstack 看哪些线程还在往里写。
按「底层原理 → 设计权衡 → 工程实践」递进排列。

第二部分:考察方向与延伸递进

考察方向:

延伸递进面试题:

  1. Full GC 后老年代能降下来,但还是频繁 Full GC,是什么原因?
  1. 堆内存不高,为什么 Full GC 还是很频繁?
  1. 怎么确认是元空间泄漏?

第三部分:面试者参考回答(三步法口语化,300-400 字)

【一句话结论】

面试官您好,频繁 Full GC 我分两步:先用"FGC 后老年代基线是否逐次抬高"区分泄漏和容量不足,再顺着 GC cause 和堆快照找到具体持有者。

【三点展开】

第一,先对齐时间线。什么时候开始的、对应哪次发布或配置变更、是不是每天固定时间出现。周期性出现优先怀疑定时任务和缓存全量刷新,这一步经常能直接省掉后面所有分析。

第二,用趋势而不是单点判断。jstat -gcutil 连续看,如果每次 FGC 后老年代都回到同一基线,只是涨得快,那是分配速率高或堆太小,调新生代和晋升阈值;如果基线逐次抬高,那才是泄漏,再去导堆。同时看 GC cause:System.gc 是有人显式调用,Metadata GC Threshold 是元空间,Humongous Allocation 是 G1 大对象,方向完全不同。

第三,定位持有者。jmap -histo:live 看 TOP 类和实例数,导堆后用 MAT 的 Dominator Tree 和 Path to GC Roots 找到是谁在持有,再用 jstack 看哪个线程在持续写入。常见的是无界本地缓存、静态集合、ThreadLocal 没 remove、监听器没反注册。

【一句边界】

当然,如果老年代不高、FGC 却频繁,那根本不是堆的问题,要往元空间、直接内存和 System.gc 上查,盯着堆看会一直找不到原因。