关于CMS GC 和 G1的一些理解

540 阅读8分钟

CMS GC

卡表 Card Table

JVM将内存分为若干大小的区域(Hotspot为512字节),每个区域就是一个Card,用一个字节数组统计CardTable(实现为一个bitmap,也有一些文章资料说是一个字节数组,应该不对)

YGC时需要知道哪些年轻代对象是被老年代引用的,这些对象不能回收,但是直接扫描全部老年代太慢了,于是这里需要利用卡表。

当卡片内部发生跨代引用的时候,写屏障(类似AOP),将CardTable对应位置改为1,也就是标记为Dirty,YGC时,扫描Dirty必然包含了老年代指向年轻代的

CMS GC的并发标记期间对象引用关系变化,也会更新card为脏页,为了防止和YGC产生冲突导致数据丢失,这时就需要mod-union table配合使用

mod-union table

一个bitmap,在YGC时,扫描了card table后,会重置card table里已经处理的标记,这样CMS 需要的信息可能被 YGC 给重置掉了,因此新增一个mod-union table,card table重置标记时,在mod-union table里相应bit位置保留脏页信息,CMS最终标记阶段使用。

流程

网上有4步的,也有7步的,其实7步的就是4步的更细节版本,多增加了并发预处理提前处理一部分DirtyCard

  1. 初始标记
    STW,标记GCROOT直达的对象(GCROOT包括方法栈中引用对象,本地方法栈引用对象,静态变量,方法区中引用的对象),trace到可达的老年代对象
  2. 并发标记
    非STW,从初始标记阶段被标记为存活的对象作为起点,向下遍历,找出所有存活的对象。 这个过程中引用关系会发生变化,发生变化的Card会标记为Dirty
  3. 最终标记
    STW,重新遍历GCROOT和根集合(这里包含全部年轻代,不管死活都算上,所以很慢, 因为并不是所有的字节码指令都会有写屏障),重新扫描标记,处理mod-union table里的DirtyCard,重新扫描标记
    参考资料: hllvm-group.iteye.com/group/topic…
  4. 并发清理
    清理未使用的对象并回收它们占用的空间

image.png

G1 GC

G1将整个堆内存分为N个Region(默认2048个), 每个Region大小1M/2M/4M/8M/16M/32M(可配置G1HeapRegionSize)

g1.png

region结构

每个Region含有五个指针 + 2个bitmap

⁃ Bottom 指向 Region 起点
⁃ Top 当前Region 分配对象的游标,Top 永远指向当前Region 最新分配的对象
⁃ PrevTAMS 和 NextTAMS 分别标记前后两次并发标记周期开始时 Top 指针的位置 (TAMS - top at mark start)
⁃ End 表示 Region 终点

previous marking bitmap: 上一轮标记后存活标记,每个bit代表一个存活对象
next marking bitmap: 本轮标记时存活标记,每个bit代表一个存活对象

g1-2.png

解释一下上面这张图
[Bottom,PrevTAMS) -> 这部分的存活信息会在previous marking bitmap体现 [PrevTAMS, NextTAMS) -> 这部分对象在第 n-1 轮全局标记周期是隐式存活; [NextTAMS, Top) -> 这部分对象在第 n 轮全局标记周期是隐式存活

g1-3.jpg

解释一下上面这张图,图中可以很明确看到两个bitmap数据结构,G1 是借助 bitmap 来存放对象存活标记,每一个 bit 表示每个region中的某个对象起始地址,如果 bit 标记为 1,则表示该对象存活,bit 与对象对应有一套算法。

SATB(Snapshot-At-The-Begin)之所以叫这个名字,就是在初始标记开始时,G1 收集器打了一个快照,形成一个所谓的对象图 (Object Graph)。这个对象图记录在 next marking bitmap 之中 ,在并发标记阶段会在这个 bitmap 中 记录对象存活标记,最终Remark阶段结束后,完成对快照对象图所有标记。

而NextTAMS 指针之后的内容,在这一次的GC 周期内并不关注,也不会被标记在此 bitmap 中。

进入到清理阶段,next marking bitmap 与 previous marking bitmap 会发生 置换(swap), next marking bitmap 在下一次周期开始前会被清空。那么此时这个 Region 的 previous marking bitmap 可以直接表示出 该Region 在 [Bottom,NextTAMS) 这个区间内存活对象数量,并且可以根据bitmap算出存活对象的具体地址,辅助下一步的 Evacuation (选取CSet ,拷贝并合并存活对象到新的region里)。回收的同时减少了内存碎片,当然 Evacuation 也是 STW的。

至此完成了一次全局并发标记周期

RSET

记录分类

  1. 新生代引用新生代 --- 不用记录,G1的三种回收算法(YGC/MIXED GC/FULL GC)都会全量处理新生代分区,所以新生代都会被遍历到。因此无需记录这种引用关系
  2. 新生代引用老年代 --- 不用记录,混合GC时,G1会采用新生代分区作为根,那么在遍历新生代分区时就能找到老年代分区了,无需这个引用关系
  3. 老年代引用新生代 --- 需要记录,YGC在回收新生代时,如果新生代的对象被老年代引用,那么需要标记为存活对象,省去扫描全部老年代(作用类似于cardTable的脏页)
  4. 老年代引用老年代 --- 需要记录。混合GC时,只会回收部分老年代,被回收的老年代需要正确的标记哪些对象存活。

作用

  1. YGC时,省去扫描全部老年代(代替CMS GC的dirty card作用)

数据结构

RSet 稀疏表->细粒度->粗粒度

稀疏表(HashTable):
key: 引用自己的region
value: 引用自己的region的card数组

细粒度(链表):
key: 引用自己的region
value:引用自己的region的cardTable的BitMap,有引用的就是1

粗粒度(BitMap):
key: region
value: 引用自己的region置为1

流程

G1 分为 YGC、 MixGC、 FullGC

YGC

新建对象会放到Eden区,当Eden耗尽时,G1会启动一次YGC,整个过程STW

  1. 执行STW,所有年轻代分区放入Collection Set
  2. 标记GC Roots,处理dirty card queue里的card, 更新RSet,使RSet保持准确, 找到老年带RSet中指向年轻代的对象
  3. 遍历存活对象树, Eden区的存活对象复制到Survivor区空的内存分段,如果Survivor区空间不够,直接复制到old区。Survivor区中对象年龄+1, 达到阈值的复制到Old区
  4. 处理引用,处理Soft、Weak、Phantom引用对象

Mix GC

old区占用达到整个堆的XX:InitiatingHeapOccupancyPercent(默认45%,JDK9变为动态)时,触发全局并发扫描,然后开启MixGC

  1. 初始标记,STW,标记GC Root直接可达对象,触发YGC,trace到所有survivor区可达的老年代对象,过程和YGC共享STW,清空快照nextBitmap(previous bit map 代表已标记完成的快照)
  2. 并发标记,非STW,trace整个对象树,同时计算每个region存活对象比例,标记到nextBitMap中,这个过程中引用关系发生变化,会把消失的引用对象放到一个堆栈中(Remember set log)
  3. 最终标记,STW,处理snapshot-at-the-beginning(SATB)中的对象,处理堆栈中的对象,配合RSet看看到底还有没有人引用它,然后做标记, 这个过程很快(因为SATB不像incremental update需要重新扫描)
  4. 独占清理,STW,交换previous marking bitmap和next marking bitmap, 通过previous marking bitmap计算各个区域的存活对象和GC回收比例,并进行排序,识别可以混合回收的区域,放入到CSET中。为下阶段做铺垫,到这里全局并发扫描完成
  5. 筛选回收,STW,全局并发标记周期结束以后,老年代中百分百为垃圾的内存分段被回收了,部分为垃圾的内存分段被计算了出来。默认情况下,这些老年代的内存分段会分8次(可以通过-XX:G1MixedGCCountTarget设置)被回收,这个过程可以并发执行,但是STW效率更快
  • 由于老年代中的内存分段默认分8次回收,G1会优先回收垃圾多的内存分段。垃圾占内存分段比例越高,越会被先回收。并且有一个阈值会决定内存分段是否被回收。-XX:G1MixedGCLiveThresholdPercent,默认为85%,意思是垃圾占内存分段比例要达到15%才会被回收(放入CSET).如果垃圾占比太低,意味着存活的对象占比高,在复制的时候会花费更多的时间。
  • 混合回收并不一定要进行8次。有一个阈值-XX:G1HeapWastePercent,默认值为5%,意思是允许整个堆内存中有5%的空间被浪费,意味着如果发现可以回收的垃圾占堆内存的比例低于5%,则不再进行混合回收。因为GC会花费很多的时间但是回收到的内存却很少。

参考资料

zhuanlan.zhihu.com/p/71058481
www.jianshu.com/p/989429f64…
cloud.tencent.com/developer/a…
blog.csdn.net/qq_34404081…