Java之CMS和G1垃圾回收过程的异同说明
作者:SnailMann
CMS 和 G1 垃圾回收过程的异同
垃圾回收
CMS 垃圾回收
CMS 垃圾收集器是基于并发-清理(Mark-Sweep) 算法,以获取最短停顿时间为目标的垃圾收集器,是 JVM 第一款真正意义上的并发收集器。CMS 垃圾回收整体分为 4 个过程
初始标记(CMS initial mark)
- STW, 暂停所有业务线程
- 标记 GC Roots 的直接关联对象
- 能找到的第一个对象,速度很快
并发标记 (CMS concurrent mark)
- 并发标记是基于初始标记得知的 GC Roots 的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长,但无需 STW, GC 线程与业务线程同时进行,会造成浮动垃圾和漏标问题
- 过程中,GC 线程会记录黑色对象引用白色对象的记录,并用于重新标记阶段的二次检查
重新标记 (CMS remark)
- 基于并发标记产生的漏标问题,开启 STW, 使用增量更新算法,将并发标记期间出现的漏标现象进行二次标记。即黑色对象被置为灰色,GC 线程重新扫描这些对象的引用路径,避免漏标
- 这个阶段的停顿会比初始标记要长
并发清理 (CMS concurrent sweep)
- GC 线程开始对白色对象
- 即无引用的垃圾进行回收
CMS 会造成的问题
- 多标问题,产生浮动垃圾
- 漏标问题, 产生 BUG
G1 垃圾回收
G1 垃圾收集器整体上采用标记-整理算法,细节上是一种复制算法,作为一种全功能全代的垃圾收集器,是 JDK9 之后的默认垃圾收集器,用于替代 CMS。垃圾回收的主要过程跟 CMS 一样,主要也是 4 个阶段
初始标记 (Inital Marking)
- STW, 仅标记 GC Roots 能直接关联到的对象,耗时很短,跟 CMS 一样
并发标记 (Concurrent Marking)
- 从 GC Roots 直接关联对象开始可达性分析,扫描对象图,耗时很长,并发执行。
- 同理会造成浮动垃圾和漏标问题;为了避免漏标问题,采用 SATB 原始快照记录下在并发时有引用变动的对象。即白色对象从灰色对象的引用中断开
最终标记 (Final Marking)
- STW, 用于处理并发标记阶段留下的少量 SATB 记录。
- 将断开的白色对象置为灰色,让 GC 线程重新扫描这些对象
筛选回收 (Live Data Counting And Evacuation)
- 负责更新 Region 的统计数据,对各个 Region 的回收价值和成本进行排序。然后根据用户预期停顿时间,自由任意选择多个 Region 构成回收集(CSet)
- STW, GC 线程并行将需要回收的 CSet 中的存活对象复制到空闲状态的 Region 中
注意解释
- 这里对筛选回收其实是一种通用简单的解释方案,实际上并不是这样;老年代回收分为两个阶段标记,一,并发标记阶段。二,垃圾回收阶段。而并发标记阶段分为 4 个阶段(初始标记,并发标记,重标记,清理阶段)。本文将并发标记阶段的清理子阶段和垃圾回收阶段合并称之为筛选回收
- 实际上清理子阶段并不处理垃圾回收的事情,仅仅统计 region 排序,得出 Cset, 不过依然要 STW; 而垃圾回收阶段才真正进行存活对象的拷贝和垃圾回收。但每次垃圾回收都是在新的一轮 YGC 开始才会发生。即不管是 YGC 还是 Mixed GC,最终只会得到要清理的 CSet, 在新的一轮 CG 来临时,才会去清理,即延迟清理。虽然我们不知道有什么好处
CMS, G1 针对三色标记漏标的策略
不管是 CMS 还是 G1, 都是采用三色标记算法来处理并发垃圾回收的问题,但对其可能会造成的漏标行为,却采用了不同的行为
增量更新策略
- 在并发标记阶段,记录由黑色对象引用白色对象的变动
- 在重新标记阶段,STW, 对黑色对象置灰,重新扫描灰色对象以及其引用路径
我的理解中,增量更新策略就类似在黑色对象引用白色对象的插入屏障中,记录这样一个信息,交给重新标记阶段去处理
SATB (snapshot-at-beginning)策略
- 在并发标记阶段,记录从灰色对象中断开的白色对象的变动,说白了就是将原有引用信息记录下来,后续处理
- 在最终标记阶段,STW 处理这些变化,将断开的白色对象置为灰色,最终等待 GC 线程扫描
我的理解是,SATB 同样是利用写屏障,只不过利用的是删除屏障,即在白色对象断开的时候,记录下原有的指向信息,因为它认为在一开始所记录的所有对象都要被扫描。同理交给最终标记阶段去处理
Golang 策略
- 插入屏障/ 删除屏障
- 混合写屏障
G1 有什么优势?为什么可以替换 CMS ?
G1 的优势
- 具有可预测的停顿时间模型,支持用户设置预期的停顿时长;即 G1 可以尽可能让停顿时间在用户希望的范围浮动
- 全新设计,全年代回收,简单高效
- 相较 CMS ,不会产生空间碎片
G1 的缺点
- 相较 CMS 需要更多的系统内存占用,比如占用总空间的 10 ~ 20 %
替换场景
- 在 CMS 上,假设在秒杀场景,一瞬间来了大量请求,如果年轻代设置的过小,会导致 Survice 区因为有对象存活,导致新对象无法进入年轻代,而被担保进老年代。逐渐积累,会导致 Full GC。尤其在高并发场景,这非常容易导致频繁的 Full GC, 这也就会造成严重的卡顿
- 在 CMS 上,解决方式也很简单,因为秒杀瞬间,大部分对象都是一会就死亡的,基本不会是老年代对象。所以我们只需要缩小老年代大小,增加年轻代大小就可以解决这个问题
- 在 G1 上,就更容易解决了, 因为年轻代大小会根据每次的 YGC 进行自动的调节。我们不需要主动设置,G1 可以自行的解决这个问题,自行选择合适的年轻代和老年代比例
问题
CMS 初始标记为什么需要 STW ?
- 如果初始标记不 STW , 在程序运行的过程中,会有不断的局部变量出现,这样我们就不知道要标记到什么时候才算结束了
- 同时初始标记耗时很短,所以即时 STW 也不会影响什么
G1 筛选回收为什么需要 STW ?
- 首先筛选回收阶段,我们需要分为两个部分,一部分是进行 Region 垃圾信息等统计,并根据排序选择指定的 CSet, 这部分需要 STW, 避免并发带来的麻烦。
- 另一部分就是垃圾清理, 因为涉及到了存活对象的复制,将对象从一块区域复制到另一块区域,如果没有 STW, 可能会导致对象覆盖的问题
在并发标记阶段,如果没有 STW, 新加入的对象要怎么处理?
新加入的对象有那么几种可能的情况
- 新加入对象是GC Roots 直接关联对象
- 新加入对象被黑色对象引用了
- 新加入对象被白色或灰色对象引用了
假设新加入对象分配为白色对象,那么就存在几个问题
- 如果是 GC Roots 直接关联对象,因为已经错过了初始标记,所以会导致新对象被认为是垃圾,而被回收掉
- 如果被黑色对象引用,且没有其他引用路径,这就造成了漏标问题,依旧会被当做是垃圾回收掉
- 如果是白色或灰色引用,那么问题不大
不管怎么说,依然会存在新加入对象被当做垃圾回收掉的问题,所以新对象直接分配为白色对象是有问题的,我们要怎么处理才合理呢?
在不同的语言,不同的垃圾收集器,我猜测应该由不同的处理方式,这里可以列举一下
- Go 1.8 中,三色标记 + 混合写屏障策略下,新加入的对象都标记为灰色。所以新对象肯定会被 GC 线程扫描。优点就是避免漏标,缺点就是可能多标,造成浮动垃圾
- 同理,CMS 或 G1 我猜测也是使用了类似方案
G1 到底使用什么垃圾回收算法?
网上通常说 G1 采用的是标记-整理算法,整体上也可以这么去理解
- 整体标记-整理,细节复制算法
- G1 会将需要回收的 region 中活跃的对象拷贝到空闲分区,然后将回收 region 进行清理,再归类为空闲分区,这一个过程像是标记整理,也像复制算法
- G1 只提供有 YGC 和 Mixed GC 选项, 如果Mixed GC实在无法跟上程序分配内存的速度,导致老年代填满无法继续进行 Mixed GC,就会使用Serial Old GC(Full GC)来收集整个GC heap。所以我们可以知道,G1是不提供Full GC的, 且要避免 Full GC
- JDK10 之间只有串行 Full GC, 11 后则由并行 Full GC 了
G1 GC 与垃圾回收过程怎么混合理解?
G1 的并发标记阶段仅仅针对与老年代对象的分析,即我们在上面描述的垃圾回收阶段是针对 G1 老年代回收的,新生代回收相比以上更加的简单。全程 STW, GC 线程并行回收新生代对象
实际上 Mixed GC 就包含了 YGC 的整个过程,即如果发生了 Mixed GC, 那么之前必然发生了 YGC。我们可以简单的理解 YGC 的过程只有两个阶段 (初始标记,筛选回收)。而在初始标记过后,当堆内存的占用到达内存可用总量的 45% (可设置),就会触发并发标记阶, 此时就代表从 YGC 进阶到 Mixed GC, 之后就走正常的流程了
Mixed GC 的开始,基于 G1 YGC 的完成,并发标记过程由并发标记线程从 Survivor 区或老年代的 RSet 记录的活跃对象作为根并开始标记
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。