垃圾回收器
垃圾回收集器分新生代收集器、老年代收集器、组合收集器;JDK8 默认:Parallel Scavenge + Parallel Old;JDK9 开始默认 G1;
新生代
- Serial(串行收集器)
单线程,STW,标记复制。
适用:客户端、单核小应用。
缺点:STW 时间长。启动参数:-XX:+UseSerialGC - Parallel Scavenge(PS,并行回收)
多线程,标记复制。目标:吞吐量优先。
吞吐量 = 运行用户代码时间 / (用户代码 + GC 时间)。
搭配老年代:Parallel Old。
参数:-XX:+UseParallelGC - ParNew
多线程,标记复制。和 PS 算法一样,但可以和 CMS 配合(PS 不能和 CMS 搭配)。
JDK8 以前 CMS 默认新生代收集器。
参数:-XX:+UseParNewGC
老年代
- Serial Old
单线程,标记整理。
用途:客户端;CMS 失败时的降级备用收集器。 - Parallel Old
多线程,标记整理;吞吐量优先。搭配 PS。
JDK8 默认组合:PS + Parallel Old。 - CMS(Concurrent Mark Sweep,并发标记清除)低延迟,并发标记 + 清除,会产生内存碎片。
阶段:初始标记 (STW) → 并发标记 → 重新标记 (STW) → 并发清除
缺点:
- 内存碎片,Full GC 后需要整理;
- 浮动垃圾;
- 并发失败(Concurrent Mode Failure)触发 Full GC,退化 Serial Old;
- 占用 CPU。
JDK9 开始废弃 CMS。
参数:-XX:+UseConcMarkSweepGC
整堆收集器(新生代 + 老年代一起回收)
- G1(Garbage-First)
分区模型(Region),不再严格分新生代老年代;可预测停顿。
算法:标记整理,整体无碎片;局部标记复制。
目标:兼顾吞吐量和低延迟。
优先回收垃圾最多的 Region。
JDK9 默认 GC。
参数:-XX:+UseG1GC - ZGC(Z Garbage Collector)
JDK11 引入,JDK15 正式启用。
目标:极低延迟,STW 几乎 ms 级。
染色指针,读屏障;几乎全并发。
最大堆支持 TB 级。几乎没有 STW。
JDK17 推荐生产使用。
参数:-XX:+UseZGC - Shenandoah
RedHat 开发,和 ZGC 类似,低延迟;
不需要移动对象时的读屏障,用转发指针。
JDK12 引入,OpenJDK。
对比表
| 收集器 | 代 | 算法 | 特点 | 目标 | 缺点 |
|---|---|---|---|---|---|
| Serial | 新生 | 标记复制 | 单线程 | 简单,小内存 | STW 长 |
| Parallel Scavenge | 新生 | 标记复制 | 多线程 | 吞吐量优先 | 停顿时间不可控 |
| ParNew | 新生 | 标记复制 | 多线程 | 配合 CMS | 已慢慢淘汰 |
| Serial Old | 老年 | 标记整理 | 单线程 | 兜底回收 | STW 很长 |
| Parallel Old | 老年 | 标记整理 | 多线程 | 吞吐量优先 | 停顿不可控 |
| CMS | 老年 | 标记清除 | 并发 | 低延迟 | 碎片、并发失败,JDK9 废弃 |
| G1 | 整堆 | 标记整理 + 复制 | 分区 Region | 可控停顿 | 大堆场景性能不如 ZGC |
| ZGC | 整堆 | 染色指针 | 全并发 | 亚毫秒级 STW | CPU 消耗略高 |
| Shenandoah | 整堆 | 转发指针 | 全并发 | 低延迟 | 生态不如 ZGC 成熟 |
垃圾回收器执行流程
CMS(Concurrent Mark Sweep,并发标记清除)
CMS 是老年代收集器,目标:低延迟,尽量并发减少 STW;算法:标记清除,会产生内存碎片。
搭配新生代:ParNew。JDK9 开始废弃。
核心:只有两个阶段 STW,其余阶段 GC 线程和业务线程并发执行
初始标记->并发标记->重新标记->并发清除
1. 初始标记(Initial Mark)【STW,很短】
只标记GC Roots 直接可达的对象(第一层对象),不会递归遍历整个对象图。
暂停所有用户线程。因为只扫根直接引用,速度很快。
2. 并发标记(Concurrent Mark)【并发,无 STW】
GC 线程和业务线程同时运行。
从初始标记得到的根对象开始,递归遍历整个老年代对象图,标记所有存活对象。
⚠️ 并发阶段业务线程还在修改引用,会产生漏标问题。CMS 使用增量更新方案解决漏标:
如果黑色对象新增指向白色对象的引用,写屏障把这个黑色对象记录到灰色栈,留到 Remark 阶段重新扫描。
3. 重新标记(Remark)【STW,时间波动大】
处理并发标记阶段产生的漏标对象。
扫描灰色栈里记录的对象,重新遍历它们的引用,补标记存活对象。
这个阶段 STW 时间不确定,如果并发阶段大量新增引用,灰色栈很大,STW 会明显变长。
4. 并发清除(Concurrent Sweep)【并发,无 STW】
GC 线程和业务线程并发运行。
遍历堆,把未被标记的对象当成垃圾直接回收,不移动存活对象。
算法是标记清除,不会整理内存,会产生内存碎片。
G1(Garbage-First)
G1:Garbage-First,分区 Region 模型,整堆收集器,目标可预测停顿。
GC 分为两类:Young GC、Mixed GC;尽量避免 Full GC(Full GC 是退化,单线程 Serial Old,STW 很长)
G1 采用SATB 原始快照解决三色标记漏标。
Young GC(年轻代回收)
触发条件:Eden 区耗尽。
- STW,暂停业务线程
- 标记 Eden、Survivor 里存活对象
- 复制存活对象到 Survivor Region;对象年龄增加,达到阈值晋升到老年代 Region
- 清空 Eden
只回收年轻代 Region,不处理老年代;Mixed GC 的初始标记会依附在 Young GC 末尾,复用这次 STW,减少一次停顿
Mixed GC(混合 GC,G1 核心,老年代占比达到 IHOP 阈值触发)
一次性回收部分年轻代 + 部分垃圾最多的老年代 Region,一共 4 个阶段:
初始标记(Initial Mark)【STW】
依附在 Young GC 之后,复用 STW。只标记 GC Roots直接可达对象。并发标记(Concurrent Mark)【并发,业务线程继续跑】
GC 线程和用户线程并发,从初始标记的对象遍历整个堆,标记存活对象。
当业务线程删除 / 覆盖引用,SATB 写屏障会把旧对象压入 SATB 队列,防止漏标。
产生浮动垃圾:快照时刻存活,但并发阶段已经失效的对象,本次 GC 不能回收。
最终标记(Remark)【STW】
短暂暂停,处理 SATB 队列剩余对象,完成全部存活对象标记。筛选回收(Cleanup & Evacuation)【STW】
① 对所有 Region 按垃圾占比排序;
② 根据-XX:MaxGCPauseMillis目标停顿时间,挑选垃圾最多的一批 Region;
③复制存活对象到空闲 Region(Evacuation 转移);
④ 清空这批 Region。
因为是复制存活对象,所以不会产生内存碎片。
⚠️ Mixed GC ≠ Full GC:Mixed GC 只挑选部分老年代 Region 回收,不是全部老年代。
G1如何解决三色指针漏标问题
G1 使用SATB 写屏障解决三色标记漏标问题。SATB 的核心思想:在初始标记完成那一刻拍一个逻辑快照,凡是快照时刻存活的对象,本轮 GC 一律当成存活,不允许被漏标回收。
漏标必要条件
- 黑色对象新增引用指向白色对象
- 灰色对象指向该白色对象的全部引用被删除
SATB 写屏障只拦截「引用删除 / 覆盖」操作。
当业务线程执行obj.field = null或者覆盖旧引用:
- 判断:被删除的旧引用指向的对象,是否是快照内对象(地址 ≤ TAMS)
- 如果是快照内对象 → 把这个对象压入SATB 队列
- 如果是快照后新建对象(>TAMS)→ 不处理
重点:SATB不捕获新增引用,只捕获删除快照内对象的引用。
TAMS(Top At Mark Start)
标记开始时的 Region 分配顶指针,G1 专属,在Mixed GC 初始标记完成瞬间,给每一个 Region 记录下当时 Region 的top值。
先搞懂 Region 里的两个指针
bottom:Region 内存起始地址top:对象分配游标,新对象从 top 往后分配;top 前面是已经分配的对象,top 后面是空闲空间。
Region 内存:
bottom | 已分配对象 | top | 空闲空间 | end
TAMS 的作用
在初始标记完成(快照点),G1 对每个 Region 保存:TAMS = 当前Region.top
以此划分 Region 内两类对象:
- 对象地址 ≤ TAMS:快照前分配的对象(快照内对象),SATB 保护范围。删除这类对象的引用,写屏障会把对象压入 SATB 队列。
- 对象地址 > TAMS:快照之后并发标记阶段新建的对象(快照外对象),不受 SATB 保护。删除它的引用,写屏障不做任何处理。
✅ G1 判断对象是否属于快照内对象,底层就是比较对象地址和所属 Region 的 TAMS。
ZGC(Z Garbage Collector)
ZGC:低延迟并发垃圾收集器,目标亚毫秒级 STW,堆可以支持 TB 级。
核心技术:染色指针(Colored Pointers) + 读屏障(Load Barrier)
ZGC 是并发标记 + 并发转移(复制),全程几乎不 STW。
ZGC 没有分代,不区分新生代老年代;没有 Region,是 Page 页管理。
三色标记保护:读屏障,和 G1 SATB、CMS 增量更新不一样。
ZGC 一次 GC 分为 4 大阶段,只有初始标记、再标记两个极短 STW 阶段,STW 时间和堆大小无关,只和 GC Roots 数量相关。
1. 初始标记(Pause Mark Start)【STW|极短】
暂停所有业务线程。
只扫描GC Roots,标记 Root 直接可达对象。
✅ STW;只找根,不递归遍历对象图。
2. 并发标记(Concurrent Mark)【并发,业务线程正常跑】
GC 线程和业务线程并发执行。
从 Root 对象开始遍历整个对象图,标记存活对象。
读屏障发挥作用:
业务线程读取对象引用时触发读屏障,检查指针染色状态;如果对象正在被移动,读屏障会转发到新地址,保证拿到正确对象。
ZGC 靠读屏障,解决并发标记时的对象引用变化问题。
3. 再标记(Pause Mark End)【STW|极短】
短暂 STW,处理并发标记阶段遗留的少量引用,完成存活对象标记。
这是第二轮 STW,时间很短。标记阶段到此结束。
4. 并发转移(Concurrent Relocate)【并发,业务线程正常跑】
ZGC 挑选垃圾多的 Page,准备回收。
并发把存活对象复制转移到新 Page(类似复制算法)。
- 对象复制完成后,旧对象指针保留染色标记;
- 后续业务线程访问旧地址时,读屏障自动转发到新对象地址;
- 等所有引用都更新到新地址,旧 Page 直接释放回内存。
转移全程并发,不会 STW。
补充:并发转移完成后,还有一个并发重置(Concurrent Reset),重置 Page,为下一轮 GC 做准备,并发执行。
ZGC如何解决三色指针漏标问题
ZGC 使用「读屏障 + 染色指针(Colored Pointers)」来解决并发标记漏标,采用的是读快照(Load Barrier)方案。
ZGC 要保证:标记开始那一刻存活的对象,本轮 GC 一定全部标记,不能漏。
ZGC 核心:染色指针(Colored Pointer)
ZGC 在对象引用指针本身,借用地址高位几个 bit 存标记状态:
- Marked0
- Marked1
- Remapped
不是对象头!是指针本身。
所以 ZGC 要求操作系统是64位且堆不能超过 4TB,否则地址位不够。
通过染色指针,ZGC 可以不用去对象头,直接从指针就能判断对象标记状态。
ZGC 的读屏障(Load Barrier)
ZGC 没有写屏障(G1/CMS 是写屏障),ZGC 用读屏障。
读屏障:每当业务代码读取对象引用的时候,触发一小段逻辑。
当线程读取引用
obj.field,读屏障会检查:
如果这个指针是未标记的快照内对象(白色),立刻把这个对象标记成存活,并且递归标记它的子对象。
ZGC并发标记核心规则
ZGC 在标记开始,选定本轮标记版本(Marked0 / Marked1,二选一交替使用)。
对象在标记开始前分配:对象引用指针上没有本轮标记版本 →快照前对象,本轮需要标记保护,防止漏标。
对象在标记开始之后分配:分配的时候直接把这个新对象的指针标记为本轮已标记(Marked0/Marked1)。
👉新对象出生即标记为存活,本轮 GC 不会去扫描、回收它,直接交给下一轮 GC。
标记举例:
- GC 周期 A 启动,选择本轮标记版本:
Marked0 - 标记开始前,对象 C 已经分配,指针无 Marked0 标记 →快照内对象
- 并发标记期间,业务 new 对象 B,分配时直接打上
Marked0→快照后对象 - 读屏障读到 C:C 没有 Marked0,属于快照内对象,如果白色,就标记存活
- 读屏障读到 B:B 自带 Marked0,快照后对象,直接放行,不处理
等到下一轮 GC 周期 B,切换标记版本为
Marked1。此时 B 上面的 Marked0 就不再是本轮标记,B 就变成本轮快照内对象。
怎么防止漏标
漏标发生的根源:业务删掉灰色引用,同时黑色新增指向白色快照对象,GC 看不到。
在 ZGC:
只要业务线程读取这条黑色对象指向白色对象的引用,读屏障就触发,当场把白色对象标记存活。
👉 只要这条引用被业务访问一次,对象就被标记,不会丢失。
ZGC 的快照承诺:标记开始时存活的对象,本轮必须保留。
标记开始之后新创建的对象,本轮 GC 完全不处理,全部留给下一轮 GC(和 G1 不一样)。