01-04-B-垃圾回收面试与生产事故实战

01-04-B-垃圾回收面试与生产事故实战

📌 导读 :这是《垃圾回收机制详解》的配套 B 篇。GC 是 Java 面试深挖的终点站------从"怎么判断对象死了"一路追问到"ZGC 为什么快",答不上来就暴露深度。本篇把高频题按追问链一问一答连贯展开 ,生产事故按场景 → 根因 → 预防 → 解决 → 一句话教训五段式复盘。原理细节标注"见 A 篇 x.x 节"。

全文约 8900 字。


📑 目录


一、高频面试题精讲(连贯问答链)

链条①:判死与引用四连问

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 用内存开销换可预测性。每个收集器都是一组取舍的答案,说出"它放弃了什么",比说出"它得到了什么"更显功力。


📌 配套阅读

上一篇:《01-03-A-JVM内存模型与对象详解.md》

 A 篇:《01-04-A-垃圾回收机制详解.md》

下一篇:《01-05-A-类加载机制详解.md》

如果这篇文章对你有帮助,欢迎点赞、收藏、关注!

相关推荐
鹿角片ljp3 小时前
KV Cache 解析
java·算法
captain3763 小时前
网络原理(9)-数据链路层
java·网络协议·java-ee
惊讶的猫4 小时前
意图识别置信度和检索重排的分数
java
caoerzhong4 小时前
仓库管理数字化趋势解读:JeeWMS 开源 Java WMS 视角下的五个演进方向
java·开源
程序员JerrySUN4 小时前
Jetson Edge AI 实战01:Nano、Xavier、Orin 怎么选?Jetson 硬件选型详解【视频讲解】
java·数据库·redis·安全·mybatis
Joy T5 小时前
Spring AI 2.0 Agent 进阶:Memory、State 与 Context Engineering 常见技术全景
java·人工智能·后端·spring·agent入门·agent state
估值探索者5 小时前
【Python量化系统工程化 #08】关了 SSH 就停?systemd 让脚本开机自启 + 异常自动拉起
java·c++·人工智能·分类·数据挖掘
天若有情6736 小时前
C++花式魔改!手写头文件复刻Java语法,彻底打破编码思维定式
java·c++·语言做语言·jac++