电影开源系统
可达性分析
垃圾回收主要是针对 Heap 空间的垃圾收集,最重要的命题是如何判定对象的存亡,即如何通过一个准确的算法对对象的存活状态进行判定或标记。
传统的引用计数算法有硬伤,无法解决循环引用的问题,进而造成内存泄露;
而可达性分析则很好避免如上问题,将 GC Roots 作为初始状态的存活对象合集(live set),从该集合出发,探索所有能被该集合直接或间接指向的对象,而最终未被探索到的对象即是死亡。
三色标记法
可达性分析进一步延伸出三色标记法。通常,GC 算法会维持一套对象图,图上节点表示对象,节点之间的连线表示对象间的引用关系,其中:
白色节点:尚未被标记的对象;
黑色节点:已经被标记,且其引用关系已经被处理;
灰色节点:已经被标记,但引用关系尚未被处理;
CMS GC
虽然 JDK 9 已经宣布废除 CMS 垃圾收集器,但 CMS 却是 JDK 8 以前(包含JDK8) 的追求低延迟 GC 的不错的可选项 。
问:CMS 是如何降低GC延时提高性能呢?








解答这个问题之前首先要知道在整个 GC 周期内哪个操作是最耗时的。在一个 GC 周期内,除了初始标记阶段(initial-mark)是单线程且 STW 的 —— 这一步只是找出 GC Roots 在老年代直接关联的对象以及新生代所有存活对象,将其标灰而已(灰色表示自身被标记,但是引用尚未处理),因此速度不慢。还有一个重要阶段,也可能是最耗时的阶段,重标记阶段(final-remark),这一阶段同样也是 STW,之所以叫重标记,需要重扫根集,就是以 GC Roots 为出发点, 完整且并行地 trace 一次,找到所有存活对象并标记之。只有在这个阶段结束之后,剩下的白色对象才能称之为 “垃圾”。如果单看上面,可能会觉得 CMS 不可能时延很低,final-remark 阶段哪怕并行操作也不会有很乐观的延迟效果。而事实是,在初始标记阶段 到 最终重标记阶段 之间,基本上中间所有阶段的努力都是并发执行的,且都是为了让重标记阶段的时延成本降到最低。
并发标记阶段(marking): 在初始标记的基础上继续并发地递归,并标记可达对象;预清理阶段(pre-cleaning):处理并发标记阶段未能够处理的引用变化,这些引用变化会通过类似 CardTable 的方式粗略地记录下来,一般CardTable是 point-out 的,在预清理阶段只需要将 脏卡(Dirty Card)拿到,遍历其中对象,并标记存活即可;可中断预清理阶段(abortable-pre-cleaning):循环执行上面的遍历操作,直到被事件中断,这里的事件可以是循环次数达到阈值、循环处理时间达到阈值等。另外,Eden 区默认情况下使用量大于2M ,才会执行此阶段逻辑,否则没意义;有了这些并发阶段的努力,最终重标记阶段才能以尽可能低的时延执行。
Garbage First
同样是分代收集,但是和传统分代不同的是,G1 给整一块Heap内存区域均匀等分了N个 Region,N 默认情况下是 2048。


其中每个 Region 都可能有着不同的角色,这里的角色表示分代的空间所属。
针对 Region 本身,需要重点理解 Region 中的五个指针⁃ Bottom 指向 Region 起点;⁃ Top 当前Region 分配对象的游标,Top 永远指向当前Region 最新分配的对象;⁃ PrevTAMS 和 NextTAMS 分别标记前后两次并发标记周期开始时 Top 指针的位置 (TAMS – top at mark start);⁃ End 表示 Region 终点。


[Bottom,PrevTAMS)-> 这部分的存活信息会在previous marking bitmap体现;
[PrevTAMS, NextTAMS)-> 这部分对象在第 n-1 轮全局标记周期是隐式存活;
[NextTAMS, Top)-> 这部分对象在第 n 轮全局标记周期是隐式存活;
问:G1 Collector 涉及的主要功能是 ?
⁃ 全局并发标记(Global Concurrent Marking):全局并发标记存活对象;
⁃ 拷贝存活对象(Evacuation):找到一个或多个 Region 中的存活对象,拷贝并合并到新的空 Region 中,copy 的过程实现了压缩,减少内存碎片化。
问:全局并发标记包含哪些阶段?全局并发标记并非GC过程,只是标记,包含以下阶段
初始标记:STW。 扫描GC Roots,标记直接可达对象并压入扫描栈(marking stack),注意此阶段会与 YGC 共享 STW; 并发标记:Concurrent。并发递归扫描 marking stack,并标记存活对象,也会扫描SATB pre-write barrier 记录的引用; 最终标记:STW。对标 CMS 的remark阶段,但是本质不同的是,这里remark非常轻量,只需要flush SATB pre-write barrier 的 buffer; 清理 :STW。清点和重置标记状态,但不拷贝任何对象。重置RSet,盘点活对象,如果没有活对象,就直接回收 Region 到free list。问:G1 会把存活对象的标记存放在哪?SATB 具体是什么呢?


图中可以很明确看到两个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的。
至此完成了一次全局并发标记周期。
问:G1 GC 模式是怎样的?仅有 YoungGC 和 MixedGC 两种模式。其中:
Young GC(STW 且 并行执行(in parallel)):以 RSet 作为根集扫描获取存活对象,将 Eden/Survivor Region中的存活对象拷贝并合并到一个新的Region里(Evacuation)以减少内存碎片;细节上讲,这种分 Region 的收集始终都会把所有 Young Gen 作为 CSet ,这也是 RSet 不负责维护 Young -> Old 的引用关系的原因。Mixed GC(STW 且 并行执行(in parallel)):与Young GC 关注 Survivor区域不同,Mixed GC 目标在全堆,且通常伴随执行一次 Young GC ;Mixed GC 不会和 全局并发标记一同执行,一般情况下当对象在 Heap 的占用超过一定阈值的时候会触发全局并发标记周期(Global Concurrent Marking),标记周期结束后,正式开启Mixed GC 。正同样借助 RSet 作为根集扫描,且 previous marking 扫描合,estbitmap 中表明存活的对象才会被扫描到 ,基于此再去这样一来,无需全堆扫描就能拿到部分存活对象。知道哪些回收的收益更高,把高收益的作为一部分。之所以能面向全堆还能保持低延迟,无外乎于两点:一来目标可控,通过控制CSet大小间接控制了回收暂停延迟;二来可以借助 RSet ,可无需全堆扫描问:RSet 是什么?有什么用?G1 GC 模式是怎样的?Card Table结构的变种,反向指针,一般的 Card Table 是 point-out (我引用了谁),而 RSet 是相反,是 point-into 的 (谁引用了我)。
作用是,大大减少了对全堆的扫描,无论 Young GC 还是 Mixed GC 都可以借助 RSet 作为根集扫描存活对象,但缺点是有可能存在浮动垃圾(float garbage),原因是你只能知道某个待收集的对象被 A 引用,但是不知道对象 A 本身是否存活。
因此上面也提及了,为了减少 float garbage,在 MixedGC 过程中中会通过 bitmap 记录的存活信息进一步筛选出真正存活的对象,进行 Evacuation。
问:G1 SATB 比起 CMS INC Update 有什么本质上的区别 ?两种算法都是在各自的垃圾收集器并发标记阶段,既然是并发,就有可能 collector 并发线程标记以后,mutator 其他线程修改了对象的引用关系,进而造成错标和漏标,错标不要紧,仅仅会增加浮动垃圾,不影响 GC 的 正确性,而漏标就严重了。一个已经被灰对象指向白对象,在并发标记阶段会被漏标的充分必要条件是:
Mutator 插入了一个 黑对象 到 该白对象的引用;Mutator 删除了所有 灰对象 到 该白对象的引用;上面两个充要条件,只要打破一个就不会被漏标。
CMS INC Update 致力于第一个条件的打破,利用 post-write barrier 记录新增或修改以后的引用对象,哪怕删除所有灰对象到该白对象的引用,remark 阶段重新回顾一次就不会漏标了;
G1 SATB 致力于第二个条件的打破,利用 pre-write barrier 记录引用关系的删除,采用最保守的做法,把变更前的引用对象记录下来,当作是存活对象,让其活过这一周期;另外之前也提及了,SATB 在初始标记的时候打了快照,NextTAMS 指针之后的的对象也在这一个周期里隐式存活,因此 G1 的 SATB 会产生更多的浮动垃圾。但是换来的好处就是,不需要像 CMS 那样 remark,再走一遍 root trace 这种相当耗时的流程。
问:G1 比起 CMS 最大的优势是?
除了上面提及的 SATB 算法在 remark 阶段延迟极低以及借助 RSet 的实现可以不做全堆扫描(G1 对大堆更友好)以外,可以节省不少我的理解最重要的是可驾驭度,G1 是可以设定GC 暂停的 target 时间的,根据预测模型选取性价比收益更高,且一定数目的 Region 作为 CSet,能回收多少便是多少。然而 CMS 似乎做不到这点。
参考资料
https://hllvm-group.iteye.com/group/topic/44381
https://hllvm-group.iteye.com/group/topic/44381#post-272188
https://tech.meituan.com/2016/09/23/g1.html
https://www.jianshu.com/p/2a1b2f17d3e4
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/cms.html
https://www.oracle.com/technetwork/tutorials/tutorials-1876574.html
苹果cms切片软件