01-04-B-垃圾回收面试与生产事故实战
📌 导读 :这是《垃圾回收机制详解》的配套 B 篇。GC 是 Java 面试深挖的终点站------从"怎么判断对象死了"一路追问到"ZGC 为什么快",答不上来就暴露深度。本篇把高频题按追问链一问一答连贯展开 ,生产事故按场景 → 根因 → 预防 → 解决 → 一句话教训五段式复盘。原理细节标注"见 A 篇 x.x 节"。
全文约 8900 字。
📑 目录
- 01-04-B-垃圾回收面试与生产事故实战
-
- 一、高频面试题精讲(连贯问答链)
- 二、生产事故案例集(五段式复盘)
-
- [事故一:导出接口 8MB 字节数组,G1 每 5 分钟 Full GC(Humongous)](#事故一:导出接口 8MB 字节数组,G1 每 5 分钟 Full GC(Humongous))
- [事故二:老年代碎片化,CMS 深夜连环 Full GC(标记-清除的债)](#事故二:老年代碎片化,CMS 深夜连环 Full GC(标记-清除的债))
- [事故三:缓存用软引用,OOM 前 GC 疯狂空转(软引用的陷阱)](#事故三:缓存用软引用,OOM 前 GC 疯狂空转(软引用的陷阱))
- [事故四:System.gc() 被 RMI 定时触发,每小时一次 2 秒停顿](#事故四:System.gc() 被 RMI 定时触发,每小时一次 2 秒停顿)
- [事故五:Metaspace 触发 Full GC 风暴,Groovy 脚本再立功(元空间水位)](#事故五:Metaspace 触发 Full GC 风暴,Groovy 脚本再立功(元空间水位))
- 三、事故速查表
- 四、面试答题万能框架
- [五、与 A 篇的知识点映射](#五、与 A 篇的知识点映射)
一、高频面试题精讲(连贯问答链)
链条①:判死与引用四连问
Q1:怎么判断对象可以被回收?为什么不用引用计数?
【30 秒电梯版】
可达性分析:从 GC Roots(栈局部变量、静态变量、常量、JNI 引用、锁持有对象等七类)出发遍历引用链,不可达即死。引用计数被弃用因为:①循环引用互相计数不归零 ②每次赋值维护计数有性能税。
【深挖版】
| 要点 | 说明 |
|---|---|
| GC Roots 七类清单要能背(A 篇 2.1)------排查内存泄漏时 MAT 的 "Path to GC Roots" 就是反向走这条链; | --- |
| 不可达 ≠ 立即死 | 还有 finalize() 的"缓刑"机会(只执行一次、不保证运行,别用它做资源清理------try-with-resources 才是正解,呼应《Java基础深水区详解》2.4); |
| 加分点 | 说出"可达性分析需要在安全点停顿做初始标记"------把判死与 STW 串起来。 |
【追问链】GC Roots 里的静态变量让你想到什么事故?→ Q2
Q2:四种引用类型及应用场景?
【30 秒电梯版】
强(永不回收,普通引用)、软(内存不足才回收,内存敏感缓存)、弱(下次 GC 就回收,ThreadLocalMap 的 key、WeakHashMap)、虚(随时回收且 get 不到,配合 ReferenceQueue 做清理通知------DirectByteBuffer 的 Cleaner)。
【深挖版】
| 要点 | 说明 |
|---|---|
| ThreadLocal 泄漏完整机理 | key 弱引用被 GC 变 null,value 是强引用还挂在线程的 ThreadLocalMap 上;线程池线程不死 → value 永存 → 泄漏 + 数据串号。所以必须 remove(《JVM内存模型与对象面试与生产事故实战》事故五); |
| 软引用做缓存已被淘汰的原因 | 回收时机不可控(内存不足才收,收之前可能先 OOM 边缘试探)、无过期/容量语义------Caffeine 全面替代; |
| 虚引用 + ReferenceQueue 是"对象死亡通知"机制 | Cleaner 靠它在堆内对象死亡时释放堆外内存(A 篇 2.1;内存篇 2.7)。 |
【追问链】对象判死后怎么清理?→ Q3
Q3:finalize() 和 try-with-resources 怎么选?
【30 秒电梯版】
无脑 try-with-resources。finalize 三宗罪:①执行时机不确定(GC 才触发,可能永远不触发)②性能差(有 finalizer 的对象要经过两轮 GC 才能回收)③可能复活对象(this 逃逸)。JDK 9 已标记 @Deprecated,JDK 18 标记 forRemoval。
【深挖版】
| 要点 | 说明 |
|---|---|
| 两轮 GC | 第一轮标记 + 入 F-Queue,Finalizer 线程执行 finalize 后,对象若被"复活"(赋给静态变量)则逃脱;第二轮才真正回收------finalizer 线程是低优先级的,堆积会拖慢回收; |
| Cleaner(JDK 9+)是 finalize 的替代品,但定位是"最后的安全网"(Netty 的 ByteBuf 泄漏警告就靠它),资源管理依然首选显式 close。 | --- |
Q4:对象一定分配在堆上吗?
【30 秒电梯版】
不一定。JIT 的逃逸分析 :对象不逃出方法 → 标量替换拆成局部变量直接在栈帧分配(出栈即消失,零 GC 成本);不逃出线程 → 锁消除。所以"所有对象都在堆上"这句话在现代 JVM 上是错的。
【深挖版】
| 要点 | 说明 |
|---|---|
| 严格说 HotSpot 没有真正的"栈上分配对象",是标量替换(对象被拆散,压根没创建完整对象)------说出这个细节是强加分项; | --- |
| 逃逸分析三优化 | 栈上分配(标量替换)、锁消除(方法内 StringBuffer 的 synchronized 直接删)、锁粗化(连续加解锁合并); |
| -XX | +DoEscapeAnalysis 默认开;-XX:-DoEscapeAnalysis 关闭后堆分配暴涨------可以自己实验验证。 |
链条②:算法与分代三连问
Q5:GC 算法有哪些?为什么新生代用复制、老年代不用?
【30 秒电梯版】
四算法:标记-清除(简单但碎片)、标记-复制(无碎片但费空间)、标记-整理(无碎片不费空间但慢)、分代收集(总纲)。新生代 98% 对象朝生夕死 → 复制成本只与活对象成正比 → 极划算;老年代存活率高 → 复制一半对象太贵且无担保空间 → 用清除或整理。
【深挖版】
| 要点 | 说明 |
|---|---|
| 新生代复制的改良 | 不是 1:1 对半分(浪费 50%),而是 Eden:S0:S1 = 8:1:1------只浪费 10%,S1 做分配担保(A 篇 2.2); |
| 碎片的实际危害 | 标记-清除后总空间够但连续空间不够,大对象分配失败 → 提前 Full GC(CMS 的死穴之一); |
| 与 MySQL 篇呼应 | 页分裂/页合并是磁盘世界的"碎片治理",GC 整理是内存世界的同款问题。 |
【追问链】Minor GC 和 Full GC 的区别与触发?→ Q6
Q6:Minor GC / Major GC / Mixed GC / Full GC 的区别?Full GC 什么时候触发?
【30 秒电梯版】
Minor GC 收新生代(Eden 满触发,频繁但快);Major GC 严格指只收老年代;Mixed GC 是 G1 特有(新生代 + 高价值老年代 Region);Full GC 整堆 + 元空间。触发五条件:老年代不足、元空间不足、System.gc()、CMS 并发失败、空间分配担保失败。
【深挖版】
| 要点 | 说明 |
|---|---|
| G1 服务出现 Full GC = 必有异常(Humongous 分配失败/晋升失败/元空间)------正常运行的 G1 应该零 Full GC,这是监控告警规则(A 篇 4.2 观测与 4.5 验证;本篇事故一); | --- |
| System.gc() 的坑 | 默认触发 Full GC;NIO/RMI 会"帮你"调用它-------XX:+ExplicitGCInvokesConcurrent 转为并发执行,别用 DisableExplicitGC 一刀切(会断掉 DirectByteBuffer 的 Cleaner 回收路径,堆外泄漏,内存篇事故三); |
| 空间分配担保 | Minor GC 前检查"老年代最大连续空间 > 历次晋升平均值",不满足就先 Full GC 腾地方------老年代碎片化时这里最容易炸。 |
【追问链】什么是 STW?为什么必须停?→ Q7
Q7:什么是 STW?安全点是什么?
【30 秒电梯版】
STW = Stop The World,GC 时暂停所有业务线程。必须停的原因:标记过程中对象图在变(业务线程改引用),不停就标不准。安全点(Safepoint)= 线程允许被暂停的特定位置(方法调用/循环回跳/异常跳转),GC 时所有线程要跑到最近的安全点才停。
【深挖版】
| 要点 | 说明 |
|---|---|
| 隐蔽停顿 | STW 总时长 = 等待所有线程进入安全点(Time To Safepoint, TTSP)+ GC 工作。大循环里没有安全点(可数循环 counted loop,如 for(int i=0; i<10000000; i++))会让 TTSP 飙升------GC 日志显示停顿 50ms,业务却卡了 500ms,元凶就是 TTSP; |
| 排查 | -Xlog:safepoint(JDK 9+)看 TTSP;解法:-XX:+UseCountedLoopSafepoints 或改循环写法; |
| ZGC 的突破本质 | 把"必须 STW 的活"(对象图修正)用染色指针+读屏障转嫁给业务线程顺手完成,STW 只剩扫 Roots 的亚毫秒操作(A 篇 2.4)。 |
链条③:收集器五连问
Q8:说说 GC 收集器的演进?
【30 秒电梯版】
一条主线"停顿目标越来越激进":Serial(单线程全停)→ Parallel(多线程全停,吞吐王)→ CMS(并发标记清除,第一次边跑边收)→ G1(Region 化 + 可设停顿目标,JDK 9 默认)→ ZGC/Shenandoah(亚毫秒停顿且不随堆增长)→ JDK 21 分代 ZGC(低停顿+分代吞吐兼得)。
【深挖版】
| 要点 | 说明 |
|---|---|
| 每代的"思路跃迁" | Parallel 是"既然要停就多叫人";CMS 是"能不一起干的活别停业务";G1 是"把堆切块,每次只收性价比最高的";ZGC 是"让业务线程顺手把 GC 的活干了"(读屏障自愈); |
| 版本事实 | CMS JDK 9 标记废弃、JDK 14 移除;G1 JDK 7u4 引入、JDK 9 默认;ZGC JDK 11 实验、15 转正、21 分代化(A 篇第一章时间线)。 |
【追问链】CMS 为什么被淘汰?→ Q9
Q9:CMS 的工作原理和三大缺陷?
【30 秒电梯版】
四阶段:初始标记(STW 短)→ 并发标记(与业务并行)→ 重新标记(STW 修正变动)→ 并发清除。三缺陷:①标记-清除算法留碎片 ,大对象分配失败退化为 Serial Old 长停顿 Full GC ②并发阶段占 CPU (默认 (核数+3)/4 线程)③Concurrent Mode Failure:并发清除时老年代被晋升填满 → 单线程 Full GC 灾难停顿。
【深挖版】
| 要点 | 说明 |
|---|---|
| 增量更新 | 并发标记时业务把黑对象指向白对象(漏标风险),CMS 记录这类引用新增,重新标记阶段重扫------写屏障记录"新增"; |
| 与 G1 的 SATB 对照 | G1 记录"被删除的引用"按开始快照算存活,重新标记更短但浮动垃圾略多(A 篇 5.2 对比表); |
| 答题升华 | CMS 证明了"并发收集可行",G1 用 Region+复制解决了它的碎片死穴------技术演进是"保留优点修复缺陷"的接力。 |
【追问链】G1 凭什么成为默认?→ Q10
Q10:G1 的核心设计?MaxGCPauseMillis 是不是越小越好?
【30 秒电梯版】
三板斧:①堆切成 ~2048 个等大 Region,分代变逻辑概念 ②跟踪每个 Region 的回收价值(空间/耗时),优先收价值最高的(Garbage First)③可预测停顿模型:按 MaxGCPauseMillis 目标决定每次收多少 Region。不是越小越好:设 10ms 会导致每次只敢收极少 Region,回收速度赶不上分配速度 → GC 更频繁甚至退化 Full GC。
【深挖版】
| 要点 | 说明 |
|---|---|
| Humongous Region | 超过 Region 一半的大对象专用连续 Region------大对象多的服务调大 G1HeapRegionSize(本篇事故一、A 篇第四章实战); |
| IHOP(InitiatingHeapOccupancyPercent,默认 45%) | 堆占用达此水位启动并发标记------老年代增长快的服务调低,给标记留时间; |
| 退化路径 | Evacuation Failure(复制活对象时没地方放)→ Full GC(JDK 10+ 并行化,仍是长停顿信号)。 |
【追问链】ZGC 为什么能做到亚毫秒?→ Q11
Q11:ZGC 为什么停顿不随堆大小增长?
【30 秒电梯版】
两项技术:染色指针 (把标记位/重映射位存进 64 位指针的高位,看指针就知道对象状态)+ 读屏障(业务线程读引用时顺手检查:指向旧地址就当场修正,self-healing)。于是"修正全堆引用"这个 O(堆大小) 的活被摊派给业务线程顺手完成,STW 只剩扫描 GC Roots(与堆大小无关,只与线程数/Roots 量相关)------所以 16G 和 16T 停顿一样短。
【深挖版】
| 要点 | 说明 |
|---|---|
| 对比 G1 | G1 的引用修正(remembered set 处理、重定位)需要 STW 且随 Region 数增长;ZGC 把它并发化了; |
| 代价 | 读屏障约 5%~10% 吞吐开销;浮动垃圾多(并发期间死的对象下轮才收)→ 内存占用要留余量; |
| JDK 21 分代 ZGC | 加入弱分代假说红利(年轻代高频小回收),分配速率高的应用吞吐显著改善------当前大堆低延迟的最优解; |
| 染色指针的前提 | Linux x86-64 的虚拟地址高位闲置------所以 ZGC 早期只支持 64 位 Linux。 |
【追问链】实际项目怎么选?→ Q12
Q12:你的项目用什么收集器?为什么?
【30 秒电梯版】
(示例答题,按真实情况改编)常规业务服务:JDK 17 + G1,堆 8G,MaxGCPauseMillis=200------停顿可控、运维成熟、团队熟悉。压测发现导出场景 Humongous 分配触发 Full GC 后,业务改流式分页 + G1HeapRegionSize 调到 16m 解决(A 篇第四章实战)。延迟敏感的新服务在试点 JDK 21 + 分代 ZGC。
【深挖版】
| 要点 | 说明 |
|---|---|
| 这题考的是决策过程不是背参数 | 说清"业务特征(堆大小/延迟要求/分配速率)→ 收集器 → 关键参数 → 观测验证"的闭环; |
| 反面教材式答题 | "网上抄了套参数"------面试官立刻追问参数含义,露馅; |
| 永远补一句 | 先有 GC 日志观测,再谈调优(Q13)。 |
链条④:调优三连问
Q13:GC 调优的一般步骤?看哪些指标?
【30 秒电梯版】
五步:①开 GC 日志(-Xlog:gc*)②分析三指标------停顿 P99(不是平均!)、GC 频率、回收效率(回收后占用是否持续爬升)③定位类型:频率高=Eden 小或分配速率高;停顿长=老年代大/碎片/Full GC;爬升=泄漏 ④对症下药(业务修复优先于参数)⑤压测验证。工具:GCEasy/GCViewer 出报告。
【深挖版】
| 要点 | 说明 |
|---|---|
| "回收后占用持续爬升"是泄漏的 GC 侧信号------与 heap dump 分析(内存篇)互相印证; | --- |
| 调优目标要先问业务 | 吞吐型(批处理)容忍长停顿要总吞吐;延迟型(API)要 P99------目标不同参数相反(Parallel vs ZGC); |
| 90% 的"GC 问题"根因在代码 | 大对象、大查询、无界缓存、频繁创建大集合------参数是止痛药,代码是病根(A 篇 6.2 第十条)。 |
【追问链】常见的 JVM 参数怎么配?→ Q14
Q14:生产环境 JVM 参数怎么配?
【30 秒电梯版】
基线模板(8G 容器、G1、JDK 17):
#mermaid-svg-UA47pEbmvans8gb3{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-UA47pEbmvans8gb3 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-UA47pEbmvans8gb3 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-UA47pEbmvans8gb3 .error-icon{fill:#552222;}#mermaid-svg-UA47pEbmvans8gb3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-UA47pEbmvans8gb3 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-UA47pEbmvans8gb3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-UA47pEbmvans8gb3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-UA47pEbmvans8gb3 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-UA47pEbmvans8gb3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-UA47pEbmvans8gb3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-UA47pEbmvans8gb3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-UA47pEbmvans8gb3 .marker.cross{stroke:#333333;}#mermaid-svg-UA47pEbmvans8gb3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-UA47pEbmvans8gb3 p{margin:0;}#mermaid-svg-UA47pEbmvans8gb3 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-UA47pEbmvans8gb3 .cluster-label text{fill:#333;}#mermaid-svg-UA47pEbmvans8gb3 .cluster-label span{color:#333;}#mermaid-svg-UA47pEbmvans8gb3 .cluster-label span p{background-color:transparent;}#mermaid-svg-UA47pEbmvans8gb3 .label text,#mermaid-svg-UA47pEbmvans8gb3 span{fill:#333;color:#333;}#mermaid-svg-UA47pEbmvans8gb3 .node rect,#mermaid-svg-UA47pEbmvans8gb3 .node circle,#mermaid-svg-UA47pEbmvans8gb3 .node ellipse,#mermaid-svg-UA47pEbmvans8gb3 .node polygon,#mermaid-svg-UA47pEbmvans8gb3 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-UA47pEbmvans8gb3 .rough-node .label text,#mermaid-svg-UA47pEbmvans8gb3 .node .label text,#mermaid-svg-UA47pEbmvans8gb3 .image-shape .label,#mermaid-svg-UA47pEbmvans8gb3 .icon-shape .label{text-anchor:middle;}#mermaid-svg-UA47pEbmvans8gb3 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-UA47pEbmvans8gb3 .rough-node .label,#mermaid-svg-UA47pEbmvans8gb3 .node .label,#mermaid-svg-UA47pEbmvans8gb3 .image-shape .label,#mermaid-svg-UA47pEbmvans8gb3 .icon-shape .label{text-align:center;}#mermaid-svg-UA47pEbmvans8gb3 .node.clickable{cursor:pointer;}#mermaid-svg-UA47pEbmvans8gb3 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-UA47pEbmvans8gb3 .arrowheadPath{fill:#333333;}#mermaid-svg-UA47pEbmvans8gb3 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-UA47pEbmvans8gb3 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-UA47pEbmvans8gb3 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UA47pEbmvans8gb3 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-UA47pEbmvans8gb3 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UA47pEbmvans8gb3 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-UA47pEbmvans8gb3 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-UA47pEbmvans8gb3 .cluster text{fill:#333;}#mermaid-svg-UA47pEbmvans8gb3 .cluster span{color:#333;}#mermaid-svg-UA47pEbmvans8gb3 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-UA47pEbmvans8gb3 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-UA47pEbmvans8gb3 rect.text{fill:none;stroke-width:0;}#mermaid-svg-UA47pEbmvans8gb3 .icon-shape,#mermaid-svg-UA47pEbmvans8gb3 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UA47pEbmvans8gb3 .icon-shape p,#mermaid-svg-UA47pEbmvans8gb3 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-UA47pEbmvans8gb3 .icon-shape .label rect,#mermaid-svg-UA47pEbmvans8gb3 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UA47pEbmvans8gb3 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-UA47pEbmvans8gb3 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-UA47pEbmvans8gb3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 8G容器·G1·JDK17 基线模板
堆:占limit约70%
-XX:InitialRAMPercentage=70.0
-XX:MaxRAMPercentage=70.0
或写死-Xms6g -Xmx6g·两者必相等
收集器:-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
别设太激进·否则新生代被压小GC更频繁
元空间:-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m
防启动期频繁Full GC+防吃光物理内存
堆外:-XX:MaxDirectMemorySize=1g
超限抛JVM内OOM有日志
好过被OS静默击杀137
兜底:-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/dump
-Xlog:gc*:file=/logs/gc.log:time,uptime:filecount=5,filesize=50m
内存账:堆6g+元空间512m+线程栈
+直接内存1g+杂项 ≈ 8g limit
堆不是越大越好·是给堆外留活路
【深挖版】
| 要点 | 说明 |
|---|---|
| 每个参数的"为什么" | Xms=Xmx 防运行期扩缩容停顿;MetaspaceSize 256m 防启动期频繁 Full GC(默认 21m 太小);MaxDirectMemorySize 防堆外静默吃满容器(exit 137);HeapDumpOnOOM 是"留全尸"的最后一道保险------全部呼应内存篇 3.1/3.2; |
| 容器内存账 | 堆 6g + 元空间 512m + 线程栈 + 直接内存 1g + 杂项 ≈ 8g limit------堆不是越大越好,是给堆外留活路; |
| 大堆(32G+)低延迟 | 换 -XX:+UseZGC -XX:+ZGenerational(JDK 21)。 |
【追问链】线上突然 GC 恶化怎么办?→ Q15
Q15:线上服务 Full GC 突然频繁,你的排查 SOP?
【30 秒电梯版】
六步:①GC 日志确认 Full GC 原因字段(Humongous/晋升失败/元空间/System.gc)②jmap -histo:live 看谁占堆(或 dump 后 MAT)③区分泄漏(回收后占用爬升)vs 大对象(byte\[\] 巨型数组)vs 元空间(类加载数)④对应处置:泄漏找 GC Roots 引用链、大对象改流式、元空间查动态类生成 ⑤止血:重启保命 + 摘流量 ⑥复盘沉淀监控告警。
【深挖版】
| 要点 | 说明 |
|---|---|
| 完整实战案例 | A 篇第四章(导出接口 8MB byte\[\] → Humongous → Full GC,三层治疗);本篇事故一/二各演示一种分支; |
| "先止血后查因"的顺序意识 | dump 大堆会 STW 数秒------生产先摘流量再 dump(jmap -dump:live 会触发 Full GC,慎用;优先 jcmd GC.heap_dump 或自动 dump); |
| 详细工具链(jstat/jmap/jcmd/arthas/MAT)留给 1.6《JVM 排查实战》篇。 | --- |
二、生产事故案例集(五段式复盘)
事故一:导出接口 8MB 字节数组,G1 每 5 分钟 Full GC(Humongous)
事故场景
订单查询服务 P99 从 80ms 恶化到 1.5s,每隔几分钟一次 1.2s 毛刺。GC 日志:Pause Full (G1 Humongous Allocation) 每 5 分钟一次------G1 服务本不该有 Full GC。
根因分析
导出接口一次 SELECT * 拉 5 万行,Jackson 序列化产生 4~8MB byte\[\];G1 Region 默认 4MB,超过一半(2MB)即 Humongous 对象需连续 Region 存放------高频大对象分配耗尽 Region,触发 Full GC(A 篇第四章完整案例;Q10 深挖)。
预防方案
① 大结果集一律流式/分页(与 MySQL 篇深分页同一条军规);② 监控规则:G1 服务 Full GC 次数 > 0 即告警;③ 大对象多的服务预设 G1HeapRegionSize=16m/32m;④ 上线前压测覆盖导出类接口。
事故解决
止血:限流导出接口。根治三层:业务改分页流式(byte\[\] 8MB→200KB,主刀)+ RegionSize 调 16m(缓冲)+ IHOP 调 35%(提前并发标记)。验证:Full GC 归零,P99 回到 90ms。
一句话教训
G1 的 Full GC 不是"内存不够",是"有东西太大了"------看到 Humongous 就去找大对象,别去调堆大小。
事故二:老年代碎片化,CMS 深夜连环 Full GC(标记-清除的债)
事故场景
JDK 8 + CMS 的老服务,每天凌晨批处理(大量创建+释放中等对象)后,白天随机出现 3~5s 的长停顿。GC 日志:concurrent mode failure + promotion failed 交替出现,随后是 Serial Old 的单线程 Full GC。
根因分析
CMS 标记-清除不整理 → 老年代碎片化:总空闲 40% 但最大连续空闲块只有 2MB;批处理晋升的中等对象(3~5MB)找不到连续空间 → promotion failed → 触发 Serial Old Full GC(单线程标记整理,3~5s 停顿)。碎片是慢性毒药,批处理是每天的引爆器(A 篇 2.2 算法缺陷;Q9)。
预防方案
① 碎片预警:-XX:CMSFullGCsBeforeCompaction=N 配合定期压缩(老方案);② 根本解:迁移 G1 (Region 间复制天然无碎片);③ 批处理错峰 + 对象生命周期优化(临时大对象改分批小对象);④ 监控 concurrent mode failure 关键字告警。
事故解决
止血:凌晨批处理前手动触发一次压缩性 Full GC(低峰期主动还债)。根治:升级 G1(JDK 8u292 的 G1 已足够成熟),碎片问题彻底消失,长停顿归零。
一句话教训
标记-清除的碎片是"分期付款"------平时不还,大对象来催收时连本带利(Serial Old 长停顿)。
事故三:缓存用软引用,OOM 前 GC 疯狂空转(软引用的陷阱)
事故场景
图片处理服务用 SoftReference<byte[]> 做内存缓存("内存不足自动释放,多优雅")。压测大流量时:GC 频率暴涨(每秒数次 Full GC)、CPU 80% 在 GC、RT 剧烈抖动,最后还是 OOM。
根因分析
软引用的回收时机是"内存不足时"------但"不足"的判定发生在 OOM 边缘:JVM 先疯狂 GC 试图腾空间(GC overhead limit exceeded 的前奏),软引用批量释放后业务又立刻重建,形成"释放-重建-再释放"的 GC 风暴。软引用没有容量上限与过期语义,它不是缓存方案,是"OOM 缓冲垫"(Q2 深挖;A 篇 2.1)。
预防方案
① 军规:业务缓存禁用软引用,一律 Caffeine(maximumSize + expireAfterWrite + 命中率统计);② 需要"内存敏感淘汰"用 Caffeine 的 weigher + 堆水位联动,而不是软引用;③ 压测必须覆盖缓存满载场景。
事故解决
迁移 Caffeine:上限 500MB(weigher 按图片字节数计)、10 分钟过期。GC 风暴消失,命中率反而从 62% 升到 91%(软引用被过早批量释放导致命中率低------第二个隐藏伤害)。
一句话教训
软引用是"快淹死时才扔行李"------缓存要的是行李清单和载重线(容量+过期),不是救生筏。
事故四:System.gc() 被 RMI 定时触发,每小时一次 2 秒停顿
事故场景
对接外部系统的服务(用了 RMI 导出接口),监控显示每小时整点准时出现一次 2s 停顿,与业务流量无关,规律得像闹钟。
根因分析
RMI 的 DGC(分布式垃圾回收)机制默认每小时调用一次 System.gc() 清理远程引用(sun.rmi.dgc.server.gcInterval 默认 3600000ms);堆 12G 的显式 Full GC 就是 2s。"整点闹钟"是 System.gc() 类事故的指纹(Q6 深挖)。
预防方案
① -XX:+ExplicitGCInvokesConcurrent:显式 GC 转为并发执行(G1/CMS 下停顿大幅缩短)------注意别用 DisableExplicitGC(会断 DirectByteBuffer 的 Cleaner 路径,堆外泄漏);② 用 RMI 就调大 gcInterval 或关闭 DGC;③ GC 日志分析时留意"与流量无关的规律性 Full GC"。
事故解决
加 ExplicitGCInvokesConcurrent,停顿从 2s 降到 50ms 级;同时把 RMI 调用改造为 HTTP(消灭根因)。
一句话教训
规律得不像事故的停顿,多半是有人在定时调 System.gc()------先抓"闹钟",再查内存。
事故五:Metaspace 触发 Full GC 风暴,Groovy 脚本再立功(元空间水位)
事故场景
规则引擎服务(Groovy 动态脚本)启动后前 10 分钟连续 5 次 Full GC,每次日志原因都是 Metadata GC Threshold;之后趋于平静但类加载数持续上涨,一周后 OOM: Metaspace(与内存篇事故二同源,本篇看 GC 视角)。
根因分析
MetaspaceSize 默认 ~21MB 是首次触发 Full GC 的水位(不是初始大小):应用类元数据很快超过 21MB → Full GC 尝试卸载类 → 卸载不多 → 水位上调 → 再超再 GC......启动期的 Full GC 风暴就是水位反复试探;而脚本持续生成新类,一周后触顶 MaxMetaspace(A 篇 2.3 触发条件②;内存篇事故二)。
预防方案
① -XX:MetaspaceSize=256m 直接设到应用稳态水位之上(消灭启动期试探性 Full GC);② MaxMetaspaceSize=512m 设硬上限(早暴露);③ 动态类生成必须缓存编译结果(内存篇事故二的根治方案);④ 监控 jvm_classes_loaded 增长斜率。
事故解决
MetaspaceSize 调 256m:启动期 Full GC 从 5 次归零。类加载泄漏按内存篇方案根治(脚本 hash 缓存)。
一句话教训
MetaspaceSize 不是"初始大小"是"报警水位"------水位设太低,JVM 就用 Full GC 反复试探给你看。
三、事故速查表
| 现象 | 可能根因 | 快速定位 | 根治方案 |
|---|---|---|---|
| G1 服务出现 Full GC | Humongous 分配 / 晋升失败 / 元空间 | GC 日志原因字段 | 大对象流式化 / RegionSize 调大 / IHOP 调低 |
concurrent mode failure(CMS) |
并发清除赶不上晋升 / 碎片 | GC 日志 + 老年代连续空间 | 迁移 G1 |
| 与流量无关的规律性 Full GC | System.gc()(RMI/NIO 定时) | 停顿时间规律性 | ExplicitGCInvokesConcurrent |
| 启动期连续 Full GC(Metadata GC Threshold) | MetaspaceSize 水位太低 | GC 日志原因字段 | MetaspaceSize=256m |
| GC 频繁 + CPU 高 + 最终 OOM | 软引用缓存风暴 / 无界缓存 | histo 看软引用/集合规模 | Caffeine 容量+过期 |
| 回收后老年代占用持续爬升 | 内存泄漏 | dump + MAT GC Roots 链 | 修引用链(static/ThreadLocal/监听器) |
| Young GC 每秒多次 | Eden 太小 / 分配速率过高 | GC 日志频率 + 晋升量 | 调大新生代 / 减少临时对象 |
| GC 日志停顿短但业务卡顿长 | TTSP 长(可数循环无安全点) | -Xlog:safepoint | UseCountedLoopSafepoints / 改循环 |
| 容器 exit 137 且 GC 正常 | 堆外泄漏(GC 管不到) | NMT / RSS-堆差值 | MaxDirectMemorySize + Netty 泄漏检测 |
四、面试答题万能框架
#mermaid-svg-ruCzo8ju7SMMD1fu{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ruCzo8ju7SMMD1fu .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ruCzo8ju7SMMD1fu .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ruCzo8ju7SMMD1fu .error-icon{fill:hsl(220.5882352941, 100%, 98.3333333333%);}#mermaid-svg-ruCzo8ju7SMMD1fu .error-text{fill:rgb(8.5000000002, 5.7500000001, 0);stroke:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-ruCzo8ju7SMMD1fu .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ruCzo8ju7SMMD1fu .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ruCzo8ju7SMMD1fu .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ruCzo8ju7SMMD1fu .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ruCzo8ju7SMMD1fu .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ruCzo8ju7SMMD1fu .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ruCzo8ju7SMMD1fu .marker{fill:#0b0b0b;stroke:#0b0b0b;}#mermaid-svg-ruCzo8ju7SMMD1fu .marker.cross{stroke:#0b0b0b;}#mermaid-svg-ruCzo8ju7SMMD1fu svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ruCzo8ju7SMMD1fu p{margin:0;}#mermaid-svg-ruCzo8ju7SMMD1fu .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ruCzo8ju7SMMD1fu .cluster-label text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-ruCzo8ju7SMMD1fu .cluster-label span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-ruCzo8ju7SMMD1fu .cluster-label span p{background-color:transparent;}#mermaid-svg-ruCzo8ju7SMMD1fu .label text,#mermaid-svg-ruCzo8ju7SMMD1fu span{fill:#333;color:#333;}#mermaid-svg-ruCzo8ju7SMMD1fu .node rect,#mermaid-svg-ruCzo8ju7SMMD1fu .node circle,#mermaid-svg-ruCzo8ju7SMMD1fu .node ellipse,#mermaid-svg-ruCzo8ju7SMMD1fu .node polygon,#mermaid-svg-ruCzo8ju7SMMD1fu .node path{fill:#fff4dd;stroke:hsl(40.5882352941, 60%, 83.3333333333%);stroke-width:1px;}#mermaid-svg-ruCzo8ju7SMMD1fu .rough-node .label text,#mermaid-svg-ruCzo8ju7SMMD1fu .node .label text,#mermaid-svg-ruCzo8ju7SMMD1fu .image-shape .label,#mermaid-svg-ruCzo8ju7SMMD1fu .icon-shape .label{text-anchor:middle;}#mermaid-svg-ruCzo8ju7SMMD1fu .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ruCzo8ju7SMMD1fu .rough-node .label,#mermaid-svg-ruCzo8ju7SMMD1fu .node .label,#mermaid-svg-ruCzo8ju7SMMD1fu .image-shape .label,#mermaid-svg-ruCzo8ju7SMMD1fu .icon-shape .label{text-align:center;}#mermaid-svg-ruCzo8ju7SMMD1fu .node.clickable{cursor:pointer;}#mermaid-svg-ruCzo8ju7SMMD1fu .root .anchor path{fill:#0b0b0b!important;stroke-width:0;stroke:#0b0b0b;}#mermaid-svg-ruCzo8ju7SMMD1fu .arrowheadPath{fill:#0b0b0b;}#mermaid-svg-ruCzo8ju7SMMD1fu .edgePath .path{stroke:#0b0b0b;stroke-width:2.0px;}#mermaid-svg-ruCzo8ju7SMMD1fu .flowchart-link{stroke:#0b0b0b;fill:none;}#mermaid-svg-ruCzo8ju7SMMD1fu .edgeLabel{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);text-align:center;}#mermaid-svg-ruCzo8ju7SMMD1fu .edgeLabel p{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-ruCzo8ju7SMMD1fu .edgeLabel rect{opacity:0.5;background-color:hsl(-79.4117647059, 100%, 93.3333333333%);fill:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-ruCzo8ju7SMMD1fu .labelBkg{background-color:rgba(243.9999999999, 220.9999999998, 255, 0.5);}#mermaid-svg-ruCzo8ju7SMMD1fu .cluster rect{fill:hsl(220.5882352941, 100%, 98.3333333333%);stroke:hsl(220.5882352941, 60%, 88.3333333333%);stroke-width:1px;}#mermaid-svg-ruCzo8ju7SMMD1fu .cluster text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-ruCzo8ju7SMMD1fu .cluster span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-ruCzo8ju7SMMD1fu div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(220.5882352941, 100%, 98.3333333333%);border:1px solid hsl(220.5882352941, 60%, 88.3333333333%);border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ruCzo8ju7SMMD1fu .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ruCzo8ju7SMMD1fu rect.text{fill:none;stroke-width:0;}#mermaid-svg-ruCzo8ju7SMMD1fu .icon-shape,#mermaid-svg-ruCzo8ju7SMMD1fu .image-shape{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);text-align:center;}#mermaid-svg-ruCzo8ju7SMMD1fu .icon-shape p,#mermaid-svg-ruCzo8ju7SMMD1fu .image-shape p{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);padding:2px;}#mermaid-svg-ruCzo8ju7SMMD1fu .icon-shape .label rect,#mermaid-svg-ruCzo8ju7SMMD1fu .image-shape .label rect{opacity:0.5;background-color:hsl(-79.4117647059, 100%, 93.3333333333%);fill:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-ruCzo8ju7SMMD1fu .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ruCzo8ju7SMMD1fu .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ruCzo8ju7SMMD1fu :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 被问GC/内存问题
① 先定框架再展开 判死→算法→
收集器→调优 30秒电梯版先给结论
② 按面试官兴趣深挖 追问原理:
GC Roots/SATB/染色指针 追问实战:
GC日志三指标+事故案例
③ 落到生产视角 调优先问业务目标:
吞吐型还是延迟型
90%的GC问题根因在代码不在参数
④ 用案例收尾 Humongous
大对象/整点System.gc闹钟
有画面感的事故胜过一切背书
加分技巧:
- 谈 ZGC 主动讲染色指针"看指针就知道对象状态"+ 读屏障自愈(Q11)------原理级理解;
- 谈 STW 主动带 TTSP 与安全点(Q7)------90% 候选人只知道"停顿=GC 干活时间";
- 谈调优先说"目标分吞吐型/延迟型,参数相反"(Q13)------架构思维;
- 被问"遇到过什么 GC 问题",用事故一(Humongous)或事故四(整点闹钟)------有画面感的案例胜过一切背书。
五、与 A 篇的知识点映射
| 本篇题目/事故 | A 篇《垃圾回收机制详解》对应章节 |
|---|---|
| Q1 可达性分析/GC Roots | 2.1 |
| Q2 四种引用 | 2.1 引用强度表 |
| Q3 finalize | 2.1(延伸) |
| Q4 逃逸分析/栈上分配 | 《JVM内存模型与对象详解》3.3(本文 2.2 分代与分配的前提) |
| Q5 四大算法 | 2.2 |
| Q6 GC 类型与触发 | 2.3 |
| Q7 STW/安全点 | 2.3 STW 段 |
| Q8 收集器演进 | 第一章时间线 |
| Q9 CMS | 2.4 CMS 段 |
| Q10 G1 | 2.4 G1 段 + 3.3 |
| Q11 ZGC | 2.4 ZGC 段 |
| Q12 选型实践 | 3.1/3.2 |
| Q13 调优步骤 | 2.5 GC 日志 + 3.3 |
| Q14 参数模板 | 3.1(联动内存篇 3.1/3.2) |
| Q15 排查 SOP | 第四章实战 |
| 事故一 Humongous | 第四章完整案例 |
| 事故二 CMS 碎片 | 2.2 标记-清除 + 2.4 CMS 缺陷① |
| 事故三 软引用风暴 | 2.1 软引用行 |
| 事故四 System.gc() | 2.3 触发条件③ |
| 事故五 Metaspace 水位 | 2.3 触发条件② |
📌 结语 :GC 面试的尽头是"权衡意识"------复制算法用空间换速度、CMS 用碎片换并发、ZGC 用吞吐换停顿、G1 用内存开销换可预测性。每个收集器都是一组取舍的答案,说出"它放弃了什么",比说出"它得到了什么"更显功力。
📌 配套阅读:
如果这篇文章对你有帮助,欢迎点赞、收藏、关注!