JVM垃圾回收详解(重点)
垃圾回收是 JVM 自动内存管理的另一半:内存区域决定「东西放哪」,垃圾回收决定「没用的东西何时清走」。本文沿「怎么判定垃圾 → 用什么算法清 → 选哪个收集器」这条主线讲透 GC。
它解决什么问题
手动管理内存的两大顽疾是泄漏和悬垂指针。Java 用 GC 把回收自动化,换来新的权衡:回收要耗 CPU,还会让应用停顿。批处理在意总吞吐,接口服务在意单次停顿,因此 JVM 没有万能收集器,选型的本质是在吞吐量与停顿时间之间找到适合业务的那一点。
核心原理
可达性分析与 GC Roots
回收前先判定对象死活。引用计数法解决不了循环引用(两对象互相引用、外部再无引用时计数非零却都该回收),主流 JVM 弃用。
HotSpot 用可达性分析:从 GC Roots 出发向下遍历,走不到的对象即为可回收。GC Roots 清单:虚拟机栈局部变量引用的对象、JNI 引用的对象、类静态字段与常量引用的对象、被同步锁持有的对象、JVM 内部引用。只要还能从「正在执行的方法、静态字段、锁」这些活锚点摸到,对象就死不了。
四种引用:给 GC 留灰色地带
强软弱虚四级引用,强度递减,回收策略逐级放宽:
Object strong = new Object(); // 强引用 |
- 强引用:默认形态,只要强可达,宁可 OOM 也不回收。
- 软引用:临近 OOM 前才回收,适合堆内存敏感的缓存。
- 弱引用:仅弱可达则下次 GC 就回收,
WeakHashMap和ThreadLocal的 key 都用它。 - 虚引用:
get()永远为 null,只用于配合ReferenceQueue接收回收通知,常跟踪堆外内存释放。
分代假说
分代收集建立在两条经验上:绝大多数对象朝生夕灭,熬过多次回收的对象往往活得久。据此堆分为新生代和老年代:新生代存活率低,用复制只搬少量幸存者;老年代存活率高,用标记-清除或整理。新对象先进 Eden,熬过 Minor GC 进 Survivor 且年龄加一,达到晋升阈值(或 Survivor 装不下、动态年龄判定触发)后进老年代。
深入机制
三种标记算法的取舍
标记-清除:标出活对象,清掉死对象。实现最直接,但耗时随对象数增长,且清完留下碎片,大对象可能找不到连续空间而提前触发 Full GC。
复制:内存对半分只用一半,回收时把活对象搬到另一半。无碎片、对象少时极快;代价是可用内存腰斩。HotSpot 新生代是改良版:Eden 加两个 Survivor 按 8:1:1,浪费仅 10%。
标记-整理:标记后让活对象向一端滑动,清掉边界外内存。无碎片但移动对象要更新引用,停顿更重,适合回收频率低的老年代。
收集器演进
Serial(复制 + Serial Old 整理)是最老的组合:单线程收集、全程 Stop The World,但没有线程切换开销,客户端小堆上反而高效。
Parallel Scavenge + Parallel Old(JDK 8 默认)换成多线程并行,目标最大化吞吐量并支持自适应调节,适合对单次停顿不敏感的批处理任务。
CMS 是 HotSpot 第一款并发收集器:初始标记(短 STW)→ 并发标记 → 重新标记 → 并发清除,首次让 GC 与用户线程同时干活。代价是:并发阶段抢 CPU;浮动垃圾只能等下轮;标记-清除的碎片积累会触发 Full GC 兜底整理。CMS 在 JDK 9 废弃、14 移除,新项目不应再用。
G1(JDK 9 起默认)不再物理连续分代,而是把堆划成若干等大 Region,各自逻辑扮演 Eden、Survivor 或老年代。回收时按「回收价值」排序,优先收垃圾最多的 Region——Garbage-First 由此得名。整体是标记-整理、Region 间是复制,基本无碎片。用户通过 -XX:MaxGCPauseMillis(默认 200ms)给停顿目标,G1 建预测模型挑选目标时间内收得完、价值最高的 Region(软目标,不保证每次达标)。超过 Region 半大的巨型对象直接分配在连续的 Humongous Region。
ZGC 把停顿压到亚毫秒级且基本不随堆增长,支持 TB 级堆。它并发完成包括对象移动在内的绝大部分工作,靠两个关键技术:染色指针把 GC 元信息编码进指针的若干比特而非对象头;读屏障让应用线程读引用时插入检查,发现对象已移动就顺手修正。ZGC 于 JDK 11 实验登场、15 转正、21 引入分代、23 起分代为默认,代价是吞吐略低、更耗 CPU。
工程实践
-XX:+UseG1GC // JDK 9+ 默认,一般无须显式开 |
排查套路:先看 GC 日志中 Minor GC 频率与耗时,再看老年代增长曲线。Minor GC 每秒多次,多半是新生代太小或分配过快;老年代涨满后频繁 Full GC,先 dump 确认是否泄漏再考虑扩容。调参是最后一步,多数「GC 问题」的真凶在代码里。
对比与权衡
| 收集器 | 算法 | 停顿特征 | 适用场景 | 状态 |
|---|---|---|---|---|
| Serial | 复制+整理 | 单线程 STW | 客户端、小堆 | 可用 |
| Parallel | 复制+整理(多线程) | 并行 STW,吞吐优先 | 批处理、计算密集 | JDK 8 默认 |
| CMS | 标记-清除 | 低停顿但有碎片 | 已无新场景 | JDK 9 废弃、14 移除 |
| G1 | Region 化,整理/复制 | 可预测停顿(软目标) | 中大堆通用服务 | JDK 9+ 默认 |
| ZGC | 染色指针+读屏障 | 亚毫秒级,与堆大小基本无关 | 大堆、超低延迟 | JDK 15 转正 |
选型一句话:小堆客户端用 Serial,要吞吐用 Parallel,服务端大堆从 G1 起步,几十上百 GB 且对停顿零容忍再上 ZGC。
面试视角
Minor GC、Major GC、Full GC 怎么区分? Minor GC 只清新生代;Major GC 通常指只清老年代;Full GC 清整个堆加方法区。监控中 Full GC 频率是核心告警指标。
为什么新生代用复制、老年代用标记-整理? 新生代存活率低,每次只搬少量幸存者,复制成本极小且无碎片;老年代存活率高且无担保空间,复制搬运量太大,只能清除或整理。
G1 的停顿目标怎么实现? G1 维护各 Region 回收价值排序,拿到 MaxGCPauseMillis 后按历史回收成本建预测模型,每次只挑目标时间内收得完、价值最高的 Region。它是软目标,不保证每次达标。
CMS 为什么被淘汰? 并发阶段抢 CPU;浮动垃圾处理不了,可能退化为 Serial Old 长停顿;碎片最终要靠 Full GC 整理。G1 在同等停顿目标下更稳。
软引用和弱引用的区别? 触发时机不同:软引用扛到临近 OOM 才回收,适合内存敏感缓存;弱引用下次 GC 就回收,与内存是否紧张无关。
小结
- 判定垃圾用可达性分析,GC Roots 是栈上引用、静态字段、常量、锁与 JNI 引用这组活锚点。
- 四种引用构成回收梯度:强保活、软扛到 OOM 前、弱见 GC 就死、虚只发通知。
- 分代假说决定算法搭配:新生代复制、老年代清除或整理;收集器演进主线是不断压缩停顿——Serial 全程暂停,G1 可预测停顿,ZGC 亚毫秒并发转移。

