概述
最早由Red Hat公司发起,目标是利用现代多核CPU的优势,减少大堆内存在GC时产生的停顿时间。随OpenJDK 12一起发布,暂停时间不依赖于堆的大小;这意味着无论堆的大小如何,暂停时间都是差不多的。
Shenandoah最初的目标是把GC停顿时间降到毫秒级,并且将对内存的支持扩展到太字节级别。为降低停顿时间,回收器需要使用更多的线程来并发处理回收任务,而要在降低停顿时间的同时能够支持更大的堆空间,回收器对CPU的多核处理能力提出更高的要求。
支持的特性:
- 解释器
- C1屏障
- C2屏障
- 引用
- JNI临界区域
System.gc()
策略
Shenandoah GC策略
策略 | GC触发的条件 | GC选择回收分区的条件 |
---|---|---|
static | 当内存可用达到一定的國值之后,将启动GC | 在进行GC时,只选择垃圾超过一定阈值的分区 |
passive | 直到发生OOM,直接启动FGC | 在进行GC时,只选择垃圾超过一定阈值的分区,并且分区在转移后仍然能保留一定的预留空间 |
aggressive | 总是启动GC | 在进行GC时,只要分区有垃圾就会启动GC。判断分区是否有垃圾的条件:分区中垃圾超过一定的阈值;分区中可用的对象所占的空间小于一定的阈值 |
adaptive | 默认选项。当内存可用达到一定的阈值之后,或从上次GC到现在已经使用的内存超过一定阈值后,或根据使用的内存预测可用的内存不足以支撑到下一次GC时,将启动GC | 在进行GC时,只选择垃圾超过一定阈值的分区,并且分区在转移后仍然能保留一定的预留空间 |
compact | 当内存可用达到一定的阈值之后,或从上次GC到现在已经使用的内存超过一定阈值后,将启动GC | 在进行GC时,只选择垃圾超过一定阈值的分区,并且分区在转移后仍然能保留一定的预留空间。与自适应策略相比,它的阈值更低,回收的分区更多。 |
traversal | 启动遍历GC,是正常GC的优化版,把重定向和标记进行合并。GC触发时机和adaptive策略相同 | 回收的分区策略也和adaptive策略相同 |
GC策略决定Shenandoah何时启动GC以及在GC时回收哪些分区。
算法
Shenandoah为了更好地管理内存,实现4类GC算法:
- 正常回收算法(Normal GC):GC的过程通常按照初始标记、并发标记、再标记、并发转移、结束转移的步骤执行
- 降级回收算法(Degenerated GC):在GC过程中,如果遇到内存分配失败,将进入降级回收。降级回收实质上是在STW中进行的并行回收
- 全回收算法(Full GC):如果在降级回收中再次遇到内存分配失败的情况,将进入全回收。和G1的并行FGC非常类似。
- 遍历回收算法(Traversal GC):GC过程按照初始遍历、并发遍历、预清理和结束遍历的步骤执行。
降级回收算法和并行FGC类似,都是在STW中进行的。它和FGC最大的区别就是降级回收会重用当前已经并发执行的标记、转移或者重定位的信息,从当前GC失败的地方继续向下执行。
正常回收
Shenandoah中正常回收有两种模式:一般模式和优化模式。区别在于是否在标记时执行重定位,在标记的过程中执行重定位,则称为优化模式,否则称为一般模式。可通过参数ShenandoahUpdateRefsEarly
控制,可能值为off/false,表示GC执行优化模式;on/true/adaptive,表示执行一般模式。
一般模式GC步骤:
- 初始标记:Init Mark,从根集合出发,标记根集合所有引用的对象,这些对象作为第二步并发标记的出发点。会触发STW
- 并发标记:Concurrent Marking,以第一步标记的对象作为出发点,开始并发地标记对象
- 预清理:在进入再标记阶段之前,先处理引用对象,把仍然活跃的引用对象重新激活,不进行真正的GC。在该阶段是并发执行的,但是只有一个并发工作线程执行预清理
- 再标记:在该阶段要做3件事:终止标记、计算回收集、转移根集合直接的引用对象。会触发STW
- 清理:再标记结束后,部分分区可能已经没有任何活跃对象,这些分区就可以被回收
- 并发转移:Concurrent Evacuation,根据转移集,对所有在转移集中的活跃对象进行转移
- 初始重定位:Init Update Refs,初始重定位将根据SATB算法重置分区中对象分配的起始内存地址位置。会触发STW
- 并发重定位:遍历不属于回收集合中的分区的对象,根据brook pointer更新对象的引用指针
- 结束重定位:遍历根集合中所有引用的对象,更新对象的引用指针。会触发STW
- 再清理:因回收集合中对象全部转移完成,所以可以释放空间。
整个GC活动图:
优化模式GC的步骤:
- 初始标记:和一般模式中初始标记相同
- 并发标记:以第一步标记的对象作为出发点,开始并发地标记对象,注意在这一步中首先判断对象是否需要重定位,如果需要,则进行重定位
- 预清理:和一般模式中预清理相同
- 再标记:在该阶段主要做4件事,分别为更新根集合中所有对象的引用、终止标记、计算回收集、转移根集合直接的引用对象。在STW中进行
- 清理:和一般模式中清理相同
- 并发转移:和一般模式中并发转移相同
- 结束转移:设置转移结束标记,重置TLAB等信息。在STW中进行
Shenandoah中的优化模式和ZGC的GC过程基本类似,把标记阶段和重定位阶段合并。正常回收在运行的过程中,应用程序和GC线程都可能需要分配内存空间,也都有可能遇到内存不足导致分配失败的情况,此时正常回收将进入降级回收状态,如果在降级回收时再遇到内存不足,将进入FGC状态。
正常回收、降级回收和FGC交互图:
遍历回收
Shenandoah遍历回收就是把并发标记、并发转移和并发重定位合并到一个阶段。
Shenandoah遍历回收的步骤:
- 初始遍历:初始遍历待回收集合,从根集合出发,标记根集合所用引用的对象,这些对象作为第二步并发标记的出发点。会触发STW
- 并发遍历:根据第一步标记的对象作为出发点,开始并发地标记对象
- 结束遍历:该阶段主要做两件事,分别为标记SATB队列中新增的引用对象、终止标记。会触发STW
- 清理:清理可回收的分区
遍历回收算法,思路简单但实现复杂。需要解决两个问题:
- 如何选择带回收的分区?在进入优化回收时,第一次GC还没有足够的信息表明分区对象的存活情况,所以需要在下一次GC时才能选择哪些分区能够被回收。
- 如何保持一致性?主要有3种一致性问题:遍历一致性、数据一致性和重定位一致性。
- 遍历一致性:如何在遍历时正确地处理对象关系图的变化?
- 数据一致性:如何保证读的时候总是访问最新的数据?如何保证写的时候能访问到正确的对象?
- 重定位一致性:如何避免一般的成员变量更新和重定位成员变量更新的竞争?
Shenandoah中解决一致性问题的方法到也比较简单。通过执行顺序的不同来保证一致性。具体来说在标记时,如果对象需要更新引用,则先更新引用,如果没有引用更新,则判断是否需要转移,如果需要转移则转移,最后才能标记对象。
延伸
对比G1、ZGC
G1、ZGC和Shenandoah的异同点
对比项 | G1 | ZGC | Shenandoah GC |
---|---|---|---|
内存连续性 | 堆内存基于分区实现,最小分区为1MB,最大分区可达32MB | 基于分页设计,类似于G1分区。页面有3种类型,2MB、32MB和N*MB | 基于分区理论实现,最小分区为256KB,最大分区可为32MB |
是否支持分代 | 支持 | 不支持 | 不支持 |
是否全量回收 | 新生代分区在 YGC/MixedGC/FGC 中会被全部回收老生代在 Mixed GC 时部分回收,在 FGC 时全部回收 | 部分页面回收 | 部分分区回收 |
屏障 | 支持读屏障和写屏障,读屏障是为了正确标记对象,写屏障是为了处理代际引用关系管理(G1中有单独的线程处理代际引用) | 仅需要支持读屏障即可实现并发标记、并发转移和并发重定位 | 支持读屏障和写屏障,读屏障是为了对象正确标记和并发处理读数据写屏障是为了并发处理同时写数据、实际上Shenandoah还需要支持比较屏障(比较屏障是通过两个读屏障实现的) |
GC时并发性 | 目前只支持并发标记 | 支持并发标记、并发转移和并发重定位 | 支持并发标记、并发转移和并发重定位 |
GC触发策略 | 使用衰减停顿模型预测是否启动GC | 提供多种触发GC的规则,其中最为常见的触发方式是根据正态分布预测的内存分配的情况触发 | 提供多种触发GC的策略:static、passive、aggressive、compactadaptive |
NUMA支持 | 尚未支持,提案JEP 345为G1支持NUMA | 支持 | 不支持 |
字符串去重 | 支持 | 不支持 | 支持 |
引用处理 | 并行处理 | 并发处理 | 并行处理 |
在GC时非常类似,都实现并发垃圾标记、并发转移、并发重定位、压缩堆空间。Shenandoah更加类似于G1,把G1中的并行转移变成并发转移。
ZGC并发的基础是地址多视图映射。
Shenandoah并发的基础是在对象头增加一个额外数据Brook pointer,在实现读写屏障时通过Brook pointer访问对象。
参考
- 新一代垃圾回收器ZGC设计与实现