2垃圾收集器
垃圾收集器

可以搭配使用
- Serial/Serial Old
- Serial/CMS
- ParNew/Serial Old
- ParNew/CMS
- Parallel Scavenge/Serial Old
- Parallel Scavenge/Parallel Old
其中Serial Old作为CMS出现”Concurrent Mode Failure”失败的后备预案
Serial收集器
特征
- 单线程收集器,进行垃圾收集时,必须暂停所有用户线程。
- 在Client模式下的默认新生代收集器
优点
- 简单高效(与其他收集器的单线程相比)
ParNew收集器(Parallel New)
特征
- Serial的多线程版本
- 在Server模式下首选的新生代收集器
- 除了Serial,只有它可以与CMS收集器配合
- 单cpu下,不会比Serial有更好的效果,由于线程切换的开销
Parallel Scavenge收集器
- 特征
- 新生代收集器,与ParNew很相似
- 主要关注点在达到一个可控制的吞吐量,吞吐量:CPU用于运行用户代码的时间与CPU总消耗时间的比值。吞吐量优先收集器
- 可以通过参数来使虚拟机自动控制堆中新生代和老年代的比例及大小(自适应策略)。
Serial Old收集器
特征
- Serial收集器的老年代版本
- 给Client模式下的虚拟机使用
两大用途
- 在JDK1.5以前与Parallel Scavenge搭配使用
- 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用
Parallel Old收集器
特征
- Parallel Scavenge收集器的老年代版本
- 与Parallel Scavenge配合,适用于注重吞吐量及CPU资源敏感的场合
CMS收集器(Concurrent Mark Sweep)

专注于老年代回收,并发低停顿收集器,以实现最短 STW 时间为目标的收集器
回收步骤
- 初始标记(CMS inital mark)————-stop the word
- 标记GC Roots能直接关联到的对象,速度很快
- 并发标记(CMS concurrent mark)——-与用户线程并发执行
- 进行GC Roots Tracing过程
- 重新标记(CMS mark)——————-stop the word
- 修正并发标记期间因用户程序继续运行而导致标记变动的那一部分对象的标记记录
- 并发清除(CMS concurrent sweep)—–与用户线程并发执行
- 并发清除死亡的对象
缺点
- CPU资源敏感
- 默认启动的回收线程数是 (CPU数量+3)/4
- 隐含:当处理器核心不足4个时,CMS对用户程序的影响可能会很大
- 无法处理浮动垃圾,可能出现Concurrent Mode Failure
- 浮动垃圾:并发清理过程中产生的垃圾。
- 基于以上原因,无法等到老年代被填满在进行垃圾清理,需要预留空间给浮动垃圾。1.6后,是92%,预留8%的空间
- 预留内存无法满足需要会启用Serial Old来重新进行老年代的垃圾收集
- 默认启动的回收线程数是 (CPU数量+3)/4
- CMS 采用的是标记清除法,这种方法会产生大量的内存碎片,这样会给大内存分配带来很大的麻烦,如果无法找到足够大的连续空间来分配对象,将会触发 Full GC,这会影响应用的性能。当然我们可以开启 -XX:+UseCMSCompactAtFullCollection(默认是开启的),用于在 CMS 收集器顶不住要进行 FullGC 时开启内存碎片的合并整理过程,内存整理会导致 STW,停顿时间会变长,还可以用另一个参数 -XX:CMSFullGCsBeforeCompation 用来设置执行多少次不压缩的 Full GC 后跟着带来一次带压缩的。
CMS日志
- 执行命令:java -Xms20M -Xmx20M -XX:+PrintGCDetails -XX:+UseConcMarkSweepGC com.mashibing.jvm.gc.T15_FullGC_Problem01
- [GC (Allocation Failure) [ParNew: 6144K->640K(6144K), 0.0265885 secs] 6585K->2770K(19840K), 0.0268035 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
ParNew:年轻代收集器
6144->640:收集前后的对比
(6144):整个年轻代容量6585 -> 2770:整个堆的情况
(19840):整个堆大小
1 | |
三色标记算法
- 三色标记算法(Tri-color Marking) 是一种用于垃圾回收(GC)过程中追踪对象存活状态的核心算法,尤其在并发标记阶段(Concurrent Marking)中发挥关键作用。它的核心目标是通过颜色标记对象的状态,高效且安全地识别存活对象,同时支持垃圾回收器与应用程序线程的并发执行,减少停顿时间(STW)。
- 颜色分类
- 白色:表示对象没有被垃圾收集器访问过,在可达性分析的开始阶段,所有的对象都是白色的,若在分析结束阶段仍是白色的,则说明不可达
- 灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用没有被扫描过
- 黑色:表示对象已经被垃圾收集旗访问过,且这个对象的所有引用都已经扫描过,它是安全存活的,如果有其它对象引用指向了黑色对象,无须重新扫描一遍,黑色对象不可能直接指向某个白色对象
- 漏标的两种情况
- 在remark过程中,黑色指向了白色,如果不对黑色对象进行重新扫描,则会漏标会把白色D对象当做没有新引用指向从而回收掉。
- 并发标记过程中,删除了所有从灰色到白色的引用,会引起漏标,此时白色对象应该被回收
- 漏标解决方案
- Incremental Update 增量更新,关注引用的增加,把黑色重新标记为灰色,下次重新扫描属性
- STAB snapshot at begining(原始快照):关注引用的删除,当灰色到白色的引用消失时,要把引用推到GC的堆栈,保证还能被GC扫描到。
G1 收集器(Garbage-First)
可预测、可控的停顿。用户可以指定期望停顿时间,G1 会将停顿时间控制在用户设定的停顿时间以内。
回收算法是STAB + 三色标记
从整体看是使用的标记-清除算法,到那时从局部看是基于标记-复制算法实现
关键概念
- region:
- region是把连续的Java堆划分为多大大小相等的独立区域
- 每一个region都可以根据需要,扮演新生代的Edoen空间、Survivor空间或者老年代空间。收集器对扮演不同角色的Region采用不同的策略去处理
- 大小通过参数可以设定,范围是1MB到32MB,且应为2的N次幂
- 内存回收的最小单元,优先回收垃圾最多的Region(“Garbage-First”名称的由来)
- 每个Region上都维护有一份卡表,导致内存占用比其他收集器较高,6到8GB以上在考虑使用G1
- humongous区域
- 专门用来存储大对象,只要大小超过了一个Region容量一半的对象即判定为大对象
- Remember Set
- 记录指向本 Region 内对象的所有外部引用(跨 Region 引用)的来源信息
- G1 回收是以 Region 为单位进行的(尤其是 CSet)。当回收一个 Region (比如 Region A) 时,为了找出 Region A 中所有存活对象,需要扫描所有可能引用 Region A 中对象的 GC Roots。如果没有 RSet,就需要扫描整个堆(包括所有其他 Region)来找出这些引用,这在堆很大时效率极低。所以使用Remember set来记录这个
- RSet的价值在于使得垃圾收集器不需要扫描整个堆来确认谁引用了当前分区的对象,只需要扫描Rest即可。
- Collection Set
- 在一次垃圾收集暂停(STW 阶段)中,G1 决定要回收的 Region 的集合。
- 在CSet中存活的数据会在GC过程中被转移到另一个可用分区,CSet中的分区可以来自Eden空间、survivor空间或者老年代。
- CSet会占用不到整个堆空间的1%大小
- 年轻代收集 (Young GC): CSet 只包含所有 Eden Regions 和本次参与收集的 Survivor Regions。
- 混合收集 (Mixed GC): CSet 包含:
- 所有 Eden Regions 和参与收集的 Survivor Regions(年轻代部分)。
- 在并发标记周期中被识别出来的、包含可回收空间比例高(即垃圾多) 的部分 Old Regions
- 卡表
- 目的:解决跨代/跨区域引用问题。 当进行部分回收(如只回收年轻代/YGC,或只回收特定Region)时,需要快速找出老年代/其他Region中哪些对象引用了年轻代/目标Region中的对象(这些被引用的对象是存活的根)。避免扫描整个老年代/堆。
- 卡页(Card Page): 堆内存被逻辑划分为固定大小(通常512字节,JVM实现相关)的连续区域,称为卡页(Card Page)。卡表中的每个字节对应堆中的一个卡页。
- 脏卡(Dirty Card): 卡表中的字节值通常只有两种状态:
0(或Clean): 表示对应的卡页在上次GC之后,没有发生指向被保护区域(如年轻代或目标Region)的引用写入。1(或Dirty): 表示对应的卡页在上次GC之后,可能发生了指向被保护区域的引用写入(即该卡页内可能存在跨代/跨区域引用)。
- G1中的使用
- 支撑 Remembered Sets (RSet) 的维护,实现高效的分区域回收(尤其是混合收集)
- 每个 Region 都有一个 Remembered Set (RSet),用于记录其他 Region 中有哪些卡页(Card)包含指向本 Region 的引用
- STAB(Snapshot-At-The-Beginning,初始快照)
- 这是 G1 在并发标记阶段使用的一种标记算法策略
- 在并发标记开始时(初始标记阶段之后),G1 会为堆里的存活对象建立一个逻辑上的“快照”。这个快照记录了标记开始时哪些对象是存活的(被视为根或从根可达)
- 解决并发标记期间应用程序线程修改对象引用关系(即堆图结构变化)导致的漏标问题。具体来说,防止在标记过程中被删除的引用(即某个对象唯一指向另一个对象的引用被置为 null 或指向其他对象)导致原本存活的对象被错误地当作垃圾回收
CMS和G1卡表的差异
特性 CMS (Concurrent Mark-Sweep) G1 (Garbage-First) 核心目标 加速年轻代收集 (YGC),找出老年代→年轻代引用 支撑 RSet 维护,实现高效分区域回收 卡表作用 直接记录哪些卡页(老年代)有指向年轻代的引用 间接:标记发生了跨Region引用写入的源卡页 最终用途 YGC时直接扫描脏卡页找老年代→年轻代引用根 作为信号,驱动 RSet 的更新 关键结构 卡表 (终点) 卡表 (起点) + RSet (终点) 记录粒度 粗粒度 (整个卡页) 通过 RSet 精粒度 (源Region的特定卡页) 维护流程 写屏障 → 直接标记卡表 写屏障 → 标记卡表 & 脏卡入队 → Refinement线程 扫描脏卡 → 更新 RSet 主要优势 避免YGC扫描整个老年代 支持高效回收任意Region,避免扫描整个堆 主要缺点 精度低(扫描无效卡页),仅服务于分代 维护链更复杂(RSet+卡表),占用更多内存和CPU - CMS的卡表: 像一个名单,直接告诉你“老年代里哪些小区块(卡页)可能有指向年轻代的线索”,YGC时你就按名单去这些小区块里翻找
- G1的卡表: 更像一个报警器和待办事项列表。它告诉你“哪些小区块(卡页)最近发生了‘对外写引用’的操作”。专门的后台工人(Refinement线程)看到报警/待办事项后,会去检查这些小区块具体写了什么引用,并把“谁(哪个Region的哪个卡页)指向了谁(哪个Region)”这个精确信息登记到目标Region的专属通讯录(RSet)里。当要回收某个Region时,直接查它的通讯录(RSet)就知道该去找谁问线索了。
回收步骤
- G1 的 GC 活动主要分为两种类型:年轻代收集和混合收集。整个过程由一个并发标记周期驱动。
年轻代收集
- 触发条件: Eden 区空间耗尽。
- STW 阶段:
- 根扫描: 扫描 GC Roots(栈帧、静态变量、JNI 句柄等)。
- 处理 Remembered Sets: 扫描 Dirty Card Queue 并更新相关的 Remembered Sets(记录老年代到年轻代的引用)。
- 对象复制:
- 存活对象从 Eden Region 和本次参与收集的 Survivor Region 被复制到新的、空的 Survivor Region 中。
- 达到晋升阈值(年龄阈值或 Survivor 空间不足)的对象会被复制到 Old Region(晋升)。
- 回收 Region: 被清空的 Eden Region 和参与收集的 Survivor Region 被回收,变为空白 Region 待重新分配。
并发标记周期
- 触发条件: 当堆占用率达到一定阈值(
-XX:InitiatingHeapOccupancyPercent,默认 45%)时,G1 会启动一个并发标记周期。这个周期不会立即进行回收,而是为后续的混合收集做准备。 - 阶段
- 初始标记(Initial Marking)——————-stop the word
- 标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start:下一个开始处的端点,为了让下一阶段的并发执行时,能正确的在可用的region中分配新对象)的值
- 速度非常快,因为它借用了年轻代收集的 STW 阶段完成(Piggybacking)
- 并发标记(Concurrent Marking)————-与用户线程并发执行
- 是从GC Roots开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行
- 最终标记(Final Marking)——————stop the word
- 是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,(处理少量的STAB记录)
- 清理(Live Data Counting and Evacuation)—-stop the word + 并发
- STW 部分:
- 基于并发标记结果,统计每个 Region 中存活对象的比例和回收能获得的空间。
- 识别出完全空闲的 Region 并立即回收。
- 根据回收价值(存活对象比例低)对 Region 进行排序,为混合收集选择最佳的 CSet。
- 并发部分: 执行内部清理和 Remembered Set 清理。
- STW 部分:
- 初始标记(Initial Marking)——————-stop the word
混合收集
- 触发条件: 并发标记周期完成后,G1 通常会启动一系列混合收集。
- 目标: 不仅收集年轻代 Region(Eden + Survivor),还会收集一部分在并发标记周期中被识别为包含大量垃圾的老年代 Region。
- 过程:
- 类似年轻代收集的 STW 阶段:根扫描、处理 Remembered Sets。
- 对象复制:
- 年轻代存活对象复制到新的 Survivor Region 或晋升到 Old Region。
- 被选入 CSet 的老年代 Region 中的存活对象被复制到其他空的 Region(通常是新分配的 Old Region) 中。
- 回收 CSet 中所有被清空的 Region。
- 通常需要多次混合收集才能回收完并发标记周期识别出的所有可回收的老年代 Region。默认是8次
FULL GC
- 触发条件: G1 的设计目标是尽量避免 Full GC。但当以下情况发生时,G1 会退化为单线程的 Serial Old 收集器进行 Full GC:
- 在并发标记周期完成前,老年代就被填满了(晋升失败或巨型对象分配失败),没有足够的 Region 供混合收集使用。
- 在复制阶段,Survivor 或 Old Region 空间不足容纳所有存活对象(分配失败)。
- 特点: STW 时间非常长,严重影响应用性能,应尽量避免。 优化 IHOP 阈值、增加堆大小、减少对象分配速率或调整 Region 大小有助于避免 Full GC。
G1日志
1 | |
G1产生FGC的解决方案
- G1的FGC在Java 10 以前是串行的,10以后是并行的
- 扩内存
- 提高CPU性能(回收的快,业务逻辑产生对象的速度固定,垃圾回收越快,内存空间越大)
- 降低MixedGC触发的阈值,让MixedGC提早发生(默认是45%)
Shenandoah
非官方的HotSpot垃圾收集器
G1的升级版本,也是基于Region布局
与G1的不同之处
- 支持并发的整理算法(多线程并行但不能与用户线程并行
- 默认不适用分代收集
- 去除了收集集,改用名为 连接矩阵(Connection Matrix)来记录region的引用关系
过程

初始标记(Initial Marking) : 与G1一样, 首先标记与GC Roots直接关联的对象, 这个阶段仍是“Stop The World”的, 但停顿时间与堆大小无关, 只与GC Roots的数量相关。
并发标记(Concurrent Marking) : 与G1一样, 遍历对象图, 标记出全部可达的对象, 这个阶段是与用户线程一起并发的, 时间长短取决于堆中存活对象的数量以及对象图的结构复杂程度。
最终标记(Final Marking) : 与G1一样, 处理剩余的SATB扫描, 并在这个阶段统计出回收价值最高的Region, 将这些Region构成一组回收集(Collection Set) 。 最终标记阶段也会有一小段短暂的停顿。
并发清理(Concurrent Cleanup) : 这个阶段用于清理那些整个区域内连一个存活对象都没有找到的Region(这类Region被称为Immediate Garbage Region) 。
并发回收(Concurrent Evacuation) : 并发回收阶段是Shenandoah与之前HotSpot中其他收集器的核心差异。 在这个阶段, Shenandoah要把回收集里面的存活对象先复制一份到其他未被使用的Region之中。 复制对象这件事情如果将用户线程冻结起来再做那是相当简单的, 但如果两者必须要同时并发进行的话, 就变得复杂起来了。 其困难点是在移动对象的同时, 用户线程仍然可能不停对被移动的对象进行读写访问, 移动对象是一次性的行为, 但移动之后整个内存中所有指向该对象的引用都还是旧对象的地址, 这是很难一瞬间全部改变过来的。 对于并发回收阶段遇到的这些困难, Shenandoah将会通过读屏障和被称为“Brooks Pointers”的转发指针来解决 。 并发回收阶段运行的时间长短取决于回收集的大小。
初始引用更新(Initial Update Reference) : 并发回收阶段复制对象结束后, 还需要把堆中所有指向旧对象的引用修正到复制后的新地址, 这个操作称为引用更新。 引用更新的初始化阶段实际上并未做什么具体的处理, 设立这个阶段只是为了建立一个线程集合点, 确保所有并发回收阶段中进行的收集器线程都已完成分配给它们的对象移动任务而已。 初始引用更新时间很短, 会产生一个非常短暂的停顿。
并发引用更新(Concurrent Update Reference) : 真正开始进行引用更新操作, 这个阶段是与用户线程一起并发的, 时间长短取决于内存中涉及的引用数量的多少。 并发引用更新与并发标记不同, 它不再需要沿着对象图来搜索, 只需要按照内存物理地址的顺序, 线性地搜索出引用类型, 把旧值改为新值即可。
最终引用更新(Final Update Reference) : 解决了堆中的引用更新后, 还要修正存在于GC Roots中的引用。 这个阶段是Shenandoah的最后一次停顿, 停顿时间只与GC Roots的数量相关。
并发清理(Concurrent Cleanup) : 经过并发回收和引用更新之后, 整个回收集中所有的Region已再无存活对象, 这些Region都变成Immediate Garbage Regions了, 最后再调用一次并发清理过程来回收这些Region的内存空间, 供以后新对象分配使用
Brooks Pointers
- 在原有对象布局结构的最前面统一增加一个新的引用字段, 在正常不处于并发移动的情况下, 该引用指向对象自己
ZGC
- 目标
- 尽可能对吞吐量影响不太大的前提下, 实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟
- 使用算法:colored pointer
- GC记录在指针上,不是记录在头部
- 42位指针 寻址空间4T
- 使用Region(又叫Page或者Zpage),与G1不同的是具有动态性-动态创建和销毁以及动态的区域容量大小
- 小型Region(Small Region) : 容量固定为2MB, 用于放置小于256KB的小对象。
- 中型Region(Medium Region) : 容量固定为32MB, 用于放置大于等于256KB但小于4MB的对象。
- 大型Region(Large Region) : 容量不固定, 可以动态变化, 但必须为2MB的整数倍, 用于放置4MB或以上的大对象。 每个大型Region中只会存放一个大对象, 这也预示着虽然名字叫作“大型Region”, 但它的实际容量完全有可能小于中型Region, 最小容量可低至4MB。 大型Region在ZGC的实现中是不会被重分配(重分配是ZGC的一种处理动作, 用于复制对象的收集器阶段, 稍后会介绍到)的, 因为复制一个大对象的代价非常高昂
堆内存布局
染色指针
Colored Pointer (Tag Pointer \ Version Pointer)
直接把标记信息记在引用对象的指针上
染色指针将64的指针中,除去不能够寻址的18位,将其高4位提取出来存储四个标志信息,通过这些标志信息可以直接从指针中看到其引用对象的三色标记状态、是否进入了重分配集(即被移动过)、是否只能通过Finalize方法才能被访问到
因为压缩了地址空间,所以ZGC能够管理的内存不可以超过4TB(2的42次幂),也不支持压缩指针
ZGC运行过程
并发标记(Concurrent Mark) : 与G1、 Shenandoah一样, 并发标记是遍历对象图做可达性分析的阶段, 前后也要经过类似于G1、 Shenandoah的初始标记、 最终标记(尽管ZGC中的名字不叫这些) 的短暂停顿, 而且这些停顿阶段所做的事情在目标上也是相类似的。 与G1、 Shenandoah不同的是, ZGC的标记是在指针上而不是在对象上进行的, 标记阶段会更新染色指针中的Marked 0、 Marked 1标志位。
并发预备重分配(Concurrent Prepare for Relocate) : 这个阶段需要根据特定的查询条件统计得出本次收集过程要清理哪些Region, 将这些Region组成重分配集(Relocation Set) 。 重分配集与G1收集器的回收集(Collection Set) 还是有区别的, ZGC划分Region的目的并非为了像G1那样做收益优先的增量回收。 相反, ZGC每次回收都会扫描所有的Region, 用范围更大的扫描成本换取省去G1中记忆集的维护成本。 因此, ZGC的重分配集只是决定了里面的存活对象会被重新复制到其他的Region中, 里面的Region会被释放, 而并不能说回收行为就只是针对这个集合里面的Region进行, 因为标记过程是针对
全堆的。 此外, 在JDK 12的ZGC中开始支持的类卸载以及弱引用的处理, 也是在这个阶段中完成的。并发重分配(Concurrent Relocate) : 重分配是ZGC执行过程中的核心阶段, 这个过程要把重分配集中的存活对象复制到新的Region上, 并为重分配集中的每个Region维护一个转发表(ForwardTable) , 记录从旧对象到新对象的转向关系。 得益于染色指针的支持, ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集之中, 如果用户线程此时并发访问了位于重分配集中的对象, 这次访问将会被预置的内存屏障所截获, 然后立即根据Region上的转发表记录将访问转发到新复制的对象
上, 并同时修正更新该引用的值, 使其直接指向新对象, ZGC将这种行为称为指针的“自愈”(SelfHealing) 能力。 这样做的好处是只有第一次访问旧对象会陷入转发, 也就是只慢一次, 对比Shenandoah的Brooks转发指针, 那是每次对象访问都必须付出的固定开销, 简单地说就是每次都慢,因此ZGC对用户程序的运行时负载要比Shenandoah来得更低一些。 还有另外一个直接的好处是由于染色指针的存在, 一旦重分配集中某个Region的存活对象都复制完毕后, 这个Region就可以立即释放用于
新对象的分配(但是转发表还得留着不能释放掉) , 哪怕堆中还有很多指向这个对象的未更新指针也没有关系, 这些旧指针一旦被使用, 它们都是可以自愈的。并发重映射(Concurrent Remap) : 重映射所做的就是修正整个堆中指向重分配集中旧对象的所有引用, 这一点从目标角度看是与Shenandoah并发引用更新阶段一样的, 但是ZGC的并发重映射并不是一个必须要“迫切”去完成的任务, 因为前面说过, 即使是旧引用, 它也是可以自愈的, 最多只是第一次使用时多一次转发和修正操作。 重映射清理这些旧引用的主要目的是为了不变慢(还有清理结束后可以释放转发表这样的附带收益) , 所以说这并不是很“迫切”。 因此, ZGC很巧妙地把并发重映射阶段要做的工作, 合并到了下一次垃圾收集循环中的并发标记阶段里去完成, 反正它们都是要遍历所有对象的, 这样合并就节省了一次遍历对象图的开销。 一旦所有指针都被修正之后, 原来记录新旧对象关系的转发表就可以释放掉了。
ZGC的算法
- Colored Pointer + 读屏障(LoadBarrier)

