GC算法之G1-PART X
Stage of concurrent marking
Initial mark
在初始标记期间,mutator线程会停止,以便于标记 Java 堆中所有可由根直接访问的对象(也称为根对象)。
根对象是可以从 Java 堆外部访问的对象;例如:native stack对象和 JNI(Java 本机接口)本地或全局对象。
由于 mutator 线程已停止,因此初始标记阶段是一个停止世界阶段。
此外,由于年轻代收集也会追踪根对象并且是停止世界的,因此初始标记与常规年轻代收集同时进行很方便(并且省时)。这也称为“搭便车”。
Root region scanning
在为每个区域设置 TAMS 后,mutator 线程重新启动,G1 GC 现在与 mutator 线程并发工作。
为了标记算法的正确性,需要扫描在初始标记与年轻代收集期间复制到幸存者区域的对象并将其视为标记根。 G1 GC 因此开始扫描幸存者区域。从幸存者区域引用的任何对象都被标记。因此,以这种方式扫描的幸存区域被称为“根区域”。
根区域扫描阶段必须在下一次垃圾收集暂停之前完成,因为在扫描整个堆中的活动对象之前,需要识别和标记从幸存区域引用的所有对象。
Concurrent Marking
当应用程序改变其对象图时,在标记开始阶段时的可达对象(且属于快照的一部分)可能会在标记线程发现和跟踪它们之前被覆盖。因此,SATB 标记保证要求做修改的mutator线程在 SATB 日志队列/缓冲区中记录需要修改的指针的修改之前的值。这称为“并发标记/SATB写前屏障”。
写前屏障能够记录对象引用字段的旧值,以便并发标记可以标记对象哪怕该对象在标记阶段被修改。
SATB 缓冲区的初始大小为 256 条记录,每个应用程序线程都有一个 SATB 缓冲区。如果SATB 缓冲区满了无法放入新的记录,则线程的当前 SATB缓冲区会被retire并被放置到已填充 SATB 缓冲区的全局列表中,jvm会为线程分配一个新的 SATB 缓冲区,并记录 pre_val。
只有在NTAMS下面的对象才会被标记和计数。在此阶段结束时,next标记位图被清除,以便在下一个标记周期开始时准备就绪。这是与mutator线程并发完成的。
Remark
remark阶段是最后的标记阶段。在这个 stop-the-world 阶段,G1 GC 查看所有剩余的 SATB 日志缓冲区并处理任何更新。 G1 GC 还会遍历任何未访问的活动对象。
Cleanup
在清理阶段,两个标记位图交换角色:next标记位图成为previous标记位图,previous标记位图成为next标记位图(将用在下一个周期)。 同样,PTAMS 和 NTAMS 互换角色。
清理暂停的三个主要贡献是识别完全空闲区域、对堆区域进行排序以识别用于混合垃圾收集的有效旧区域以及 RSet 清理。
当前的启发式算法根据以下因素对区域进行排名:区域的活动对象占比(具有大量活动对象的区域收集起来非常昂贵,因为复制是一项昂贵的操作)和记忆集大小(同样,具有大记忆集的区域收集起来很昂贵)。
目标是首先收集/疏散被认为成本较低(活动对象较少,记忆集小(活跃度低))的候选区域。
识别每个区域中的活动对象的一个好处是,在遇到一个完全空闲的区域时,可以清除其记忆集,并立即回收该区域并返回到空闲列表区域而不是等待混合回收。
RSet 清理还有助于检测过时的引用。例如,如果标记发现特定卡片上的所有对象都已死亡,则该特定卡片的条目将从对应的RSet 中清除。
Evacuation failures and full collection
有时 G1 GC 在尝试从年轻区域复制活动对象时或在从旧区域撤离期间尝试复制活动对象时无法找到空闲区域。
这种故障在 GC 日志中被报告为to-space exhausted failure。
故障持续时间在日志中进一步显示为 Evacuation Failure 时间。
111.912: [GC pause (G1 Evacuation Pause) (young) (to-space exhausted),0.6773162 secs]<snip>[Evacuation Failure: 331.5 ms]
还有一些时候,可能无法在老年代中找到连续的区域来分配巨大对象。
在这种时候,G1 GC 将尝试扩展 Java堆空间。如果 Java 堆空间扩展不成功,G1 GC 会触发其故障安全机制并回退到串行(单线程)full collection。
在full collection期间,单个线程对整个堆进行操作,对所有区域进行标记、清除和压缩。
full collection是一个非常昂贵的收集,尤其是在堆大小相当大的情况下。因此,强烈建议在经常发生完整收集的情况下进行调优优化。