JVM 垃圾回收基础:从 STW 到分代收集(附 G1/ZGC 导读)

运行时五区详见前作 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 或快照保证一致性):

  1. 虚拟机栈中引用的对象(各线程栈帧的局部变量表)
  2. 方法区中静态变量引用的对象(类加载后 static 字段指向的对象)
  3. 方法区中常量引用的对象(如字符串常量池中的引用)
  4. 本地方法栈中 JNI 引用的对象
  5. 被 synchronized 锁持有的对象
  6. 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)

  1. 绝大多数对象朝生夕死(如临时变量、请求 DTO)
  2. 熬过多次 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

设计原因

  1. 复制算法需要两块 Survivor 交替(From / To),保证始终有一块空 Survivor 接收存活对象
  2. Eden 大、Survivor 小:符合「大部分对象第一次 GC 就死」的假设,提高空间利用率
  3. 对象年龄 :每熬过一次 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 区空间不足(新对象分配失败)。

过程(复制算法):

  1. STW
  2. 标记 Eden + From Survivor 中的存活对象
  3. 复制到 To Survivor;年龄达标 → 晋升老年代
  4. 清空 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 放不下存活对象,老年代也放不下

监控信号jstatFGC 次数频繁增加、老年代使用率持续 >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 原理与选型对比。

相关推荐
MrSYJ1 小时前
TCP协议理解
后端·tcp/ip
boolean的主人1 小时前
超实用!5 个 MySQL 索引优化实战场景(附 10 万测试数据)
后端
BBmmo1 小时前
JDBC基础篇
后端
用户64278006937881 小时前
elpis-core 第一阶段学习心得与收获
后端
kfaino1 小时前
码农的AI翻身·前传 一个大模型从出生到上岗的全过程
后端·aigc
IT_陈寒1 小时前
Vue的这个响应式陷阱让我熬到凌晨三点
前端·人工智能·后端
葫芦和十三2 小时前
图解 MongoDB 17|大集合与工作集:数据超过内存怎么办
后端·mongodb·面试
kfaino9 小时前
码农的AI翻身(三)你好,我叫 Embedding
后端·ai编程
葫芦和十三10 小时前
图解 MongoDB 18|复制集拓扑:Primary、Secondary 和 Arbiter 的分工
后端·mongodb·面试