JVM低延迟垃圾收集器:Shenandoah
EN: JVM Low-Latency Garbage Collector : Shenandoah
一 、Shenandoah简介
一款不由Oracle(包括以前的Sun)公司的虚拟机团队所领导开发的HotSpot垃圾收集器。只有OpenJDK才会包含,而OracleJDK里反而不存在的收集器,“免费开源版”比“收费商业版”功能更多。
2014年RedHat把Shenandoah贡献给了OpenJDK,并推动它成为OpenJDK 12的正式特性之一,也就是后来的JEP 189。
目标:实现一种能在任何堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的垃圾收集器。
二、Shenandoah特征及实现思路:
Shenandoah主要特征:
(1)基于Region的堆内存布局,去除传统分代。
(2)回收策略优先处理回收价值最大的Region
(3)支持并发的整理算法。
Shenandoah收集器的实现思路:
(1)基于Region的堆内存布局,默认不使用分代收集,(相比G1的记忆集(RemberSet)处理跨代指针时维护消耗)
改用名为“连接矩阵”(Connection Matrix)的全局数据结构来记录跨Region的引用关系。降低了伪共享问题的发生概率。
连接矩阵可以简单理解为一张二维表格,如果RegionN有对象指向Region M,就在表格的N行M列中打上一个标记,如图3-15所示,如果Region 5中的对象Baz引用了Region 3的Foo,Foo又引用了Region 1的Bar,那连接矩阵中的5行3列、3行1列就应该被打上标记。在回收时通过这张表格就可以得出哪些Region之间产生了跨代引用。
(2)使用转发指针(Brooks Pointer)和读写屏障来实现与用户线程并发的整理算法。
转发指针是在原有对象布局结构的最前面统一增加一个新的引用字段,在正常不处于并发移动的情况下,该引用指向对象自己。
当对象拥有了一份新的副本时,只需要修改一处指针的值,即旧对象上转发指针的引用位置,使其指向新对象,便可将所有对该对象的访问转发到新的副本上。这样只要旧对象的内存仍然存在,未被清理掉,虚拟机内存中所有通过旧引用地址访问的代码便仍然可用,都会被自动转发到新对象上继续工作。
三、Shenandoah收集器的工作过程(九阶段划分):
1. 始标记(Initial Marking)
(A)与G1一样,首先标记与GC Roots直接关联的对象.
(B)需要“Stop The World”,有停顿,但停顿时间与堆大小无关,只与GC Roots的数量相关。
2. 并发标记(Concurrent Marking)
(A)重要的并发过程之一。
(B)与G1一样,遍历对象图,标记出全部可达的对象。可达性分析。
(C)与用户线程并发执行,时间长短取决于堆中存活对象的数量以及对象图的结构复杂程度。
3. 最终标记(Final Marking)
(A)与G1一样,处理剩余的SATB扫描,并在这个阶段统计出回收价值最高的Region,将这些Region构成一组回收集(Collection Set)。
(B)会有一小段短暂的停顿。
4. 并发清理(Concurrent Cleanup)
(A)清理那些整个区域内连一个存活对象都没有找到的Region(这类Region被称为Immediate Garbage Region)。
5. 并发回收(Concurrent Evacuation)
(A)重要的并发过程之一。并发回收阶段是Shenandoah与之前HotSpot中其他收集器的核心差异。
(B)Shenandoah要把回收集里面的存活对象先复制一份到其他未被使用的Region之中。
(C)使用读屏障和被称为”Brooks Pointers”的转发指针实现并发过程,并发回收阶段运行的时间长短取。
6. 初始引用更新(Initial Update Reference)
(A)并发回收阶段复制对象结束后,把堆中所有指向旧对象的引用修正到复制后的新地址,这个操作称为引用更新。
(B)此阶段实际上并未做什么具体的处理,设立这个阶段只是为了建立一个线程集合点,确保所有并发回收阶段中进行的收集器线程都已完成分配给它们的对象移动任务而已。
(B)初始引用更新时间很短,会产生一个非常短暂的停顿。
7. 并发引用更新(Concurrent Update Reference)
(A)重要的并发过程之一。
(B)真正开始进行引用更新操作,与用户线程一起并发的,时间长短取决于内存中涉及的引用数量的多少。
(C)并发引用更新与并发标记不同,它不再需要沿着对象图来搜索,只需要按照内存物理地址的顺序,线性地搜索出引用类型,把旧值改为新值即可。
8. 最终引用更新(Final Update Reference)
(A)解决了堆中的引用更新后,还要修正存在于GCRoots中的引用。
(B)此阶段是Shenandoah的最后一次停顿,停顿时间只与GC Roots的数量相关。
9. 并发清理(Concurrent Cleanup)
经过并发回收和引用更新之后,整个回收集中所有的Region已再无存活对象,这些Region都变成Immediate Garbage Regions了,最后再调用一次并发清理过程来回收这些Region的内存空间,供以后新对象分配使用。
Shenandoah收集器的重要的三个并发过程:
- 并发标记
- 并发回收
- 并发引用更新
Shenandoah收集器的工作流程图:
图片来源:《Shenandoah GC Part I: The Garbage Collector That Could》
说明:
黄色区域代表的是被选入回收集的Region。
绿色部分就代表还存活的对象。
蓝色区域就是用户线程可以用来分配对象的内存Region。
四、GC收集器比较
1、Shenandoah 与其他收集器并发比较
图片来源:《Shenandoah GC Part I: The Garbage Collector That Could》
黄色:表示必须挂起用户线程
绿色:表示收集器线程与用户线程并发执行。
结论:
(1)CMS和G1之前的全部收集器,其工作的所有步骤都会产生“Stop TheWorld”式的停顿;
(2)CMS和G1分别使用增量更新和原始快照(SATB)技术,实现了标记阶段的并发,不会因管理的堆内存变大,要标记的对象变多而导致停顿时间随之增长。但是对于标记阶段之后的处理,仍未得到妥善解决。
(3)CMS使用标记-清除算法,虽然避免了整理阶段收集器带来的停顿,但是清除算法不论如何优化改进,在设计原理上避免不了空间碎片的产生,随着空间碎片不断淤积最终依然逃不过“Stop The World”的命运。
(4)G1虽然可以按更小的粒度进行回收,从而抑制整理阶段出现时间过长的停顿,也有暂停。
(5)Shenandoah只有初始标记、最终标记这些阶段有短暂的停顿,这部分停顿的时间基本上是固定的,与堆的容量、堆中对象的数量没有正比例关系。
2、Shenandoah 与G1比较
Shenandoah | G1(Garbage First) | |
不同 | Shenandoah(目前)是默认不使用分代收集的,不会有专门的新生代Region或者老年代Region的存在,没有实现分代。 | G1仍然保留新生代和老年代概况,只是变成Region区组成的逻辑连续的动态集合。 |
使用“连接矩阵”(Connection Matrix)的全局数据结构来记录跨Region的引用关系。 | 使用记忆集(Rember Set)避免全堆作为GC Roots扫描,每个Region都维护有自己的记忆集。 | |
Shenandoah通过读屏障和被称为“Brooks Pointers”的转发指针实现并发回收,支持与用户线程并发。 | G1的回收阶段是可以多线程并行的,但却不能与用户线程并发。 | |
相同 | (1)都是基于Region的堆内存布局,都有用于存放大对象的Humongous Region (2)并发回收思路高度一致, (3)默认的回收策略都是优先处理回收价值最大的Region (4) 都采用标记-整理,标记-复制算法回收。 |
四、相关概念
写屏障:
在赋值操作前后,加入一些处理(类似针对写操作进行AOP的处理)
目的通常是为了记录对象引用的变动情况
读屏障:
在读值操作前后,加入一些处理(类似针对读操作进行AOP的处理)
目的通常是为了读取对象更新引用后的原有直。
说明:读写屏障没有找到相关科学定义,写屏障的理解来自网络,读屏障是根据写屏障的理解暂作说明的,如果不当,欢迎指正。
指针转发 Brooks Pointer(Forwarding Pointer / Indirection Pointer)
相关历史简介:
1984年,Brooks提出使用转发指针来实现对象移动与用户程序并发的一种解决方案。
Brooks提出的新方案不需要用到内存保护陷阱,而是在原有对象布局结构的最前面统一增加一个新的引用字段,在正常不处于并发移动的情况下,该引用指向对象自己。当对象拥有了一份新的副本时,只需要修改一处指针的值,即旧对象上转发指针的引用位置,使其指向新对象,便可将所有对该对象的访问转发到新的副本上。这样只要旧对象的内存仍然存在,未被清理掉,虚拟机内存中所有通过旧引用地址访问的代码便仍然可用,都会被自动转发到新对象上继续工作,
指针转发如图:
Brooks形式的转发指针在设计上决定了它是必然会出现多线程竞争问题的,如果收集器线程与用户线程发生的只是并发读取,那无论读到旧对象还是新对象上的字段,返回的结果都应该是一样的,这个场景还可以有一些“偷懒”的处理余地;但如果发生的是并发写入,就一定必须保证写操作只能发生在新复制的对象上,而不是写入旧对象的内存中。以下三件事情并发进行时:
1)收集器线程复制了新的对象副本;
2)用户线程更新对象的某个字段;
3)收集器线程更新转发指针的引用值为新副本地址。
如果不做任何保护措施,让事件2在事件1、事件3之间发生的话,将导致的结果就是用户线程对对象的变更发生在旧对象上,所以这里必须针对转发指针的访问操作采取同步措施,让收集器线程或者用户线程对转发指针的访问只有其中之一能够成功,另外一个必须等待,避免两者交替进行。实际上Shenandoah收集器是通过比较并交换(Compare And Swap,CAS)操作来保证并发时对象的访问正确性的。
CAS:compare and swap,解决多线程并行情况下使用锁造成性能损耗的一种机制,CAS操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在CAS指令之前返回该位置的值。CAS有效地说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。
转发指针和句柄定位比较:
转发指针 | 句柄定位 | |
不同 | 分散存放在每一个对象头前面 | 统一存储在专门的句柄池中 |
出现多线程竞争问题 | ||
相同 | 间接性的对象访问方式;对象访问会带来一次额外的转向开销 |
内存保护陷阱扩展了解:
在此之前,实现类似的并发操作,通常是在被移动对象原有的内存上设置保护陷阱(Memory Protection Trap),一旦用户程序访问到归属于旧对象的内存空间就会产生自陷中段,进入预设好的异常处理器中,再由其中的代码逻辑把访问转发到复制后的新对象上。虽然确实能够实现对象移动与用户线程并发,但是如果没有操作系统层面的直接支持,这种方案将导致用户态频繁切换到核心态。