运行时五区详见前作 JVM 内存模型与技术架构。本文沿 「STW → 如何判断垃圾 → 引用类型 → 回收算法 → 分代设计 → 收集器选型 → 触发时机」 这条主线,系统梳理 GC 基础,为下一篇 G1/ZGC 深入做铺垫。
一、模块总览:GC 学什么
| 层次 | 内容 | 本文覆盖 |
|---|---|---|
| 基础概念 | STW、可达性分析、GC Roots | ✅ |
| 算法与分代 | 标记-清除/复制/整理、新生代三区 | ✅ |
| 收集器 | Parallel / CMS / G1 / ZGC 概览 | ✅ 侧重 G1/ZGC |
| 触发时机 | Young GC / Full GC(通用)+ Mixed GC(G1 特有) | ✅ |
二、STW(Stop-The-World):GC 的「暂停键」
STW 是 GC 中的核心概念:在 GC 的某些阶段,JVM 挂起所有用户线程,只让 GC 线程工作。此时 Java 程序完全停止响应------不处理请求、不执行业务逻辑。
2.1 为什么需要 STW?
GC 需要安全地 遍历和修改对象引用图。若标记或移动对象时,用户线程仍在:
| 风险 | 场景 |
|---|---|
| 漏标 | 刚判断 C 不可达,下一秒 C 又被新引用指向 → 误回收存活对象 |
| 错标 | 标记 A 引用 B,下一秒 A 断开 → B 被误杀 |
| 内存损坏 | 对象正在移动,用户线程仍通过旧指针访问 |
因此,关键的 GC 操作必须在 STW 下完成 ,以保证正确性。现代收集器(G1、ZGC)通过 三色标记 + 写屏障,把大部分标记工作做成并发,但根扫描、部分重标记、对象转移等阶段仍需要极短 STW。
2.2 各收集器的 STW 分布
| 收集器 | STW 阶段 | 特点 |
|---|---|---|
| Serial / Parallel | 整个 GC 过程 STW(标记、清除/整理) | 停顿长,吞吐高 |
| CMS | 初始标记 + 重新标记 STW;并发标记/清除与用户线程并行 | 追求低停顿,已废弃(JDK 14+) |
| G1 | 初始标记、最终标记、Evacuation(Young/Mixed GC 转移阶段)STW | 可设 -XX:MaxGCPauseMillis 目标 |
| ZGC | 极短 STW,主要用于 根扫描 和选择性重定位 | 亚毫秒级,堆可达 TB 级 |
发展趋势 :尽量 缩短 STW 时间,而非消除 STW------完全无 STW 的 GC 在正确性上极难实现。
2.3 如何观察 STW?
方式一:GC 日志
bash
# JDK 9+ 统一日志
-Xlog:gc*:file=gc.log:time,uptime,level,tags
# 典型输出片段
[0.456s][info][gc] GC(12) Pause Young (G1 Evacuation Pause) 24M->8M(256M) 12.345ms
关注 Pause 关键字后的耗时(ms)。
方式二:监控工具
| 工具 | 用途 |
|---|---|
jstat -gc <pid> 1000 |
每秒看 YGC/FGC 次数与耗时 |
| VisualVM / JVisualVM | 图形化 GC 曲线 |
| Prometheus + Grafana + JMX Exporter | 生产环境持续监控 GC 停顿 P99 |
| Arthas | 线上 dashboard / vmoption 查看 GC 配置 |
三、如何判断对象是否是垃圾?
3.1 两种基本思路
| 方法 | 原理 | 优点 | 缺点 | 采用者 |
|---|---|---|---|---|
| 引用计数法 | 对象被引用 +1,引用失效 -1,计数为 0 则回收 | 实现简单 | 无法处理循环引用 | Python(配合其他机制)、Objective-C |
| 可达性分析法 | 从 GC Roots 出发遍历引用链,能到达=存活,不可达=垃圾 | 解决循环引用 | 需 STW 或并发标记保证一致性 | Java |
循环引用示例:
java
// 引用计数法下,a 和 b 互相引用,计数永不为 0 → 内存泄漏
Object a = new Object();
Object b = new Object();
a = b;
b = a;
a = null;
b = null; // 可达性分析:两者均不可达 → 可回收
3.2 GC Roots 有哪些?
可达性分析的 起点(必须 STW 或快照保证一致性):
- 虚拟机栈中引用的对象(各线程栈帧的局部变量表)
- 方法区中静态变量引用的对象(类加载后 static 字段指向的对象)
- 方法区中常量引用的对象(如字符串常量池中的引用)
- 本地方法栈中 JNI 引用的对象
- 被 synchronized 锁持有的对象
- JVM 内部引用(基本类型 Class、系统类加载器等)
3.3 为什么 Java 不用引用计数 + 为什么要 STW?
- 不用引用计数:循环引用无法回收;每次引用变更都要改计数,多线程下开销大
- 需要 STW(或等价机制) :可达性分析遍历过程中,引用关系若被用户线程修改 → 漏标/错标。最简单方案是 STW;现代方案是 三色标记 + 写屏障 实现并发标记(G1 用 SATB,CMS 用增量更新)
四、四种引用类型
除 GC Roots 外,Java 还定义了 四种引用强度,影响回收时机:
| 类型 | 创建方式 | 回收时机 | 典型用途 |
|---|---|---|---|
| 强引用 | Object o = new Object() |
只要强引用存在,永不回收(OOM 也不回收) | 普通对象引用 |
| 软引用 | SoftReference<T> |
内存不足时回收(在 OOM 前) | 内存敏感缓存 |
| 弱引用 | WeakReference<T> |
下次 GC 必回收 | ThreadLocal 底层、WeakHashMap |
| 虚引用 | PhantomReference<T> |
无法通过 get() 获取对象;用于跟踪回收通知 | 堆外内存回收监控、DirectByteBuffer 清理 |
注意 :软/弱/虚引用都必须通过 get() 获取对象,且随时可能返回 null,使用前需判空。
五、三种经典垃圾回收算法
| 算法 | 过程 | 优点 | 缺点 | 适用区域 |
|---|---|---|---|---|
| 标记-清除(Mark-Sweep) | 标记存活 → 统一清除垃圾 | 实现简单 | 内存碎片;两次 STW 效率一般 | CMS 老年代(并发清除) |
| 复制(Copying) | 存活对象复制到另一块内存,原区清空 | 无碎片;速度快 | 浪费一半空间(需预留 To 区) | 新生代 Minor GC |
| 标记-整理(Mark-Compact) | 标记存活 → 存活对象向一端移动 → 清理边界外 | 无碎片 | 移动对象 STW 较长 | 老年代 Full GC 兜底 |
分代收集思想:不同区域对象生命周期不同 → 用不同算法组合,而非单一算法扫全堆。
六、为什么分老年代和新生代?
分代假说(Generational Hypothesis):
- 绝大多数对象朝生夕死(如临时变量、请求 DTO)
- 熬过多次 GC 的对象倾向于长期存活(如缓存、单例)
| 区域 | 特点 | 回收算法 | 回收频率 |
|---|---|---|---|
| 新生代 | 对象新创建,存活率低 | 复制算法 | 高(Minor/Young GC) |
| 老年代 | 长期存活对象 | 标记-整理 或标记-清除 | 低(Major/Full GC) |
收益 :Minor GC 只扫小范围新生代,几十毫秒级;Full GC 才扫老年代,虽慢但触发少。G1/ZGC 在 Region 粒度上延续了这一思想。
七、新生代为什么分 Eden、S0、S1?
HotSpot 新生代默认比例 Eden : Survivor0 : Survivor1 = 8 : 1 : 1 (-XX:SurvivorRatio=8)。
css
Minor GC 前:
Eden [████████████████] S0 [██] S1 [ ]
Minor GC 后(复制算法):
Eden [ ] S0 [ ] S1 [存活对象] ← Eden+S0 存活对象复制到 S1
设计原因:
- 复制算法需要两块 Survivor 交替(From / To),保证始终有一块空 Survivor 接收存活对象
- Eden 大、Survivor 小:符合「大部分对象第一次 GC 就死」的假设,提高空间利用率
- 对象年龄 :每熬过一次 Minor GC,年龄 +1;达到
-XX:MaxTenuringThreshold(默认 15)→ 晋升老年代
动态年龄判定:若 Survivor 中同年龄对象大小超过 Survivor 一半,≥该年龄的对象直接晋升老年代。
八、常见垃圾回收器概览(侧重 G1 / ZGC)
| 收集器 | 区域 | 算法 | STW | 定位 | JDK 状态 |
|---|---|---|---|---|---|
| Serial | 新生代 | 复制 | 全程 STW | 单线程,Client 模式 | 保留 |
| ParNew | 新生代 | 复制 | 全程 STW | 配合 CMS 的多线程版 | 保留 |
| Parallel Scavenge | 新生代 | 复制 | 全程 STW | 吞吐量优先 | 默认之一 |
| Serial Old | 老年代 | 标记-整理 | 全程 STW | CMS 失败兜底 | 保留 |
| Parallel Old | 老年代 | 标记-整理 | 全程 STW | 配合 Parallel Scavenge | 默认之一 |
| CMS | 老年代 | 标记-清除 | 部分 STW | 低停顿(已废弃) | JDK 14 移除 |
| G1 | 全堆 Region | 复制+标记-整理 | 可控 STW | 低延迟 + 大堆,JDK 9 默认 | ✅ 主推 |
| ZGC | 全堆 | 染色指针+读屏障 | 极短 STW | 亚毫秒停顿,TB 级堆 | ✅ JDK 15+ 生产可用 |
选型建议:
- 通用 Web 服务、中等堆 → G1 (
-XX:+UseG1GC -XX:MaxGCPauseMillis=200) - 大堆、极低延迟(交易、实时) → ZGC (
-XX:+UseZGC) - 离线批处理、吞吐优先 → Parallel GC
九、Young GC、Full GC、Mixed GC:谁通用?谁特有?
你问的两个关键问题,结论如下:
| GC 类型 | 是否 G1 独有 | 说明 |
|---|---|---|
| Young GC(Minor GC) | ❌ 所有分代收集器都有 | Eden 满时触发,只回收新生代 |
| Full GC | ❌ 所有收集器都有 | 整堆(+ 通常含元空间)回收,STW 长 |
| Old GC / Major GC | ❌ 概念通用 | 通常指老年代回收;Serial/Parallel 中与 Full GC 常混用 |
| Mixed GC | ✅ 仅 G1 特有 | 同时回收年轻代 + 部分老年代 Region |
9.1 Young GC(Minor GC)------ 全收集器通用
触发条件 :Eden 区空间不足(新对象分配失败)。
过程(复制算法):
- STW
- 标记 Eden + From Survivor 中的存活对象
- 复制到 To Survivor;年龄达标 → 晋升老年代
- 清空 Eden 和 From Survivor,交换 S0/S1 角色
特点 :频率高、停顿短(通常 10~50ms),不是 G1 独有。
9.2 Full GC ------ 全收集器通用(不是 G1 才有)
Full GC 是对 整个 Java 堆 (及通常 元空间 )的全面回收,STW 时间长(可能数百 ms ~ 数秒)。Serial、Parallel、CMS、G1、ZGC 都会发生 Full GC(ZGC 中称为 Major GC 或特殊回收,语义类似)。
常见触发条件(通用,非 G1 独有):
| 触发场景 | 说明 |
|---|---|
| 老年代空间不足 | 晋升对象过多、大对象直接进入老年代 |
| Metaspace 不足 | 动态生成类过多(CGLIB、反射) |
| 显式调用 | System.gc()(建议 -XX:+DisableExplicitGC 禁用) |
| CMS Concurrent Mode Failure | 并发清理期间老年代增长过快,降级 Serial Old Full GC |
| G1 Evacuation Failure | 回收时找不到足够空 Region 容纳存活对象 |
| 分配担保失败 | Minor GC 后 Survivor 放不下存活对象,老年代也放不下 |
监控信号 :jstat 中 FGC 次数频繁增加、老年代使用率持续 >90%。
9.3 Mixed GC ------ 仅 G1 特有
G1 把堆划分为多个 Region 。在 并发标记周期 完成后,G1 根据各 Region 垃圾占比排序,优先回收 垃圾最多的 Region(Garbage-First 命名由来)。
Mixed GC = Young GC + 部分老年代 Region 的 Evacuation,在 -XX:MaxGCPauseMillis 目标内尽量多收垃圾。
scss
G1 并发标记周期:
初始标记(STW) → 并发标记 → 最终标记(STW) → 筛选回收
↓
Mixed GC(多次,STW Evacuation)
下一篇专题将详述 G1 七阶段流程与 ZGC 染色指针机制。
十、现代 GC 如何缩短 STW:三色标记 + 写屏障(导读)
在 STW 之外做并发标记,需解决 并发标记期间引用变更 导致的漏标问题。
三色标记:
| 颜色 | 含义 |
|---|---|
| 白色 | 未访问,默认视为垃圾 |
| 灰色 | 已访问,但其引用对象未全部扫描 |
| 黑色 | 已访问,且其引用对象均已扫描 |
写屏障(Write Barrier):引用字段被修改时插入钩子,记录变更,供 GC 线程增量处理。
| 收集器 | 策略 |
|---|---|
| CMS | 增量更新(Incremental Update)------ 记录新引用 |
| G1 | SATB(Snapshot At The Beginning)------ 保留旧引用快照 |
| ZGC | 染色指针 + 读屏障(Load Barrier)------ 访问时自动修正指针 |
这是 G1/ZGC 能把 STW 压到毫秒级的核心机制,下篇展开。
十一、全链路回顾
markdown
对象分配(Eden)
↓ Eden 满
Young GC(全收集器)── 复制算法,STW,存活→Survivor/晋升老年代
↓ 老年代渐满 / 元空间不足 / CMS 失败 ...
Full GC(全收集器)── 标记-整理/清除,长 STW
↓ G1 特有路径
并发标记 → Mixed GC(部分老年代 Region + 年轻代)
↓ 极低延迟场景
ZGC:并发标记+转移,极短 STW 根扫描
附录:GC 基础高频面试速答
Q1. 什么是 STW?为什么需要?
GC 关键阶段挂起所有用户线程;防止标记/移动对象时引用关系变化导致漏标、错标、内存损坏。
Q2. 如何判断垃圾?
Java 用可达性分析,从 GC Roots 遍历;不用引用计数(循环引用问题)。
Q3. GC Roots 有哪些?
栈引用、静态变量、常量、JNI、锁持有对象、JVM 内部引用。
Q4. 四种引用?
强引用不回收;软引用内存不足回收;弱引用下次 GC 回收;虚引用跟踪回收通知。
Q5. 三种回收算法?
标记-清除(碎片)、复制(新生代)、标记-整理(老年代)。
Q6. 为什么分代?
分代假说:多数对象朝生夕死;新生代复制快,老年代整理慢但频率低。
Q7. Eden/S0/S1 作用?
8:1:1 比例;复制算法需两块 Survivor 交替;Eden 大符合多数对象早死。
Q8. Young GC 何时触发?
Eden 满;所有分代收集器通用,非 G1 独有。
Q9. Full GC 何时触发?
老年代/元空间不足、CMS 失败、显式 System.gc() 等;全收集器通用,非 G1 独有。
Q10. Mixed GC 是什么?
G1 特有:并发标记后,同时回收年轻代 + 部分老年代 Region。
写在最后
GC 学习路径建议:本文基础(STW/可达性/分代/触发)→ 下一篇 G1 七阶段 + ZGC 染色指针 → OOM 排查与调优参数。
把「为什么 STW → 怎么判垃圾 → 为什么分代 → Young/Full 何时触发 → Mixed 是 G1 专属」串成一条线,面试就不会把 Full GC 误说成「G1 才有」。
下一篇预告:G1 垃圾回收全流程 + ZGC 原理与选型对比。