Shenandoah收集器

比起G1改进

  • 最重要的当然是支持并发的整理算法,G1的回收阶段是可以多线程并行的,但却不能与用户线程并发,这点是Shenandoah最核心的功能

  • 其次,Shenandoah(目前)是默认不使用分代收集的,换言之,不会有专门的新生代Region或者老年代Region的存在,没有实现分代,并不是说分代对Shenandoah没有价值,这更多是出于性价比的权衡,基于工作量上的考虑而将其放到优先级较低的位置上。

  • 最后,Shenandoah摒弃了在G1中耗费大量内存和计算资源去维护的记忆集,改用名为“连接矩阵”(ConnectionMatrix)的全局数据结构来记录跨Region的引用关系,降低了处理跨代指针时的记忆集维护消耗,也降低了伪共享问题的发生概率。

    • 连接矩阵可以简单理解为一张二维表格,如果Region N有对象指向Region M,就在表格的N行M列中打上一个标记

九个阶段

  • 初始标记(Initial Marking):与G1一样,首先标记与GC Roots直接关联的对象,这个阶段仍是“Stop The World”的,但停顿时间与堆大小无关,只与GC Roots的数量相关。

  • 并发标记(Concurrent Marking):与G1一样,遍历对象图,标记出全部可达的对象,这个阶段是与用户线程一起并发的,时间长短取决于堆中存活对象的数量以及对象图的结构复杂程度。

  • 最终标记(Final Marking):与G1一样,处理剩余的SATB扫描,并在这个阶段统计出回收价值最高的Region,将这些Region构成一组回收集(Collection Set)。最终标记阶段也会有一小段短暂的停顿。

  • 并发清理(Concurrent Cleanup):这个阶段用于清理那些整个区域内连一个存活对象都没有找到的Region(这类Region被称为Immediate Garbage Region)。

  • 并发撤退存活对象(Concurrent Evacuation):该阶段是Shenandoah与之前HotSpot中其他收集器的核心差异。在这个阶段,Shenandoah要把回收集里面的存活对象先复制一份到其他未被使用的Region之中。其困难点是在移动对象的同时,用户线程仍然可能不停对被移动的对象进行读写访问,移动对象是一次性的行为,但移动之后整个内存中所有指向该对象的引用都还是旧对象的地址,这是很难一瞬间全部改变过来的。Shenandoah是通过读屏障和被称为“Brooks Pointers”的转发指针来解决。该阶段运行的时间长短取决于回收集的大小。

  • 初始引用更新(Initial Update Reference):并发回收阶段复制对象结束后,还需要把堆中所有指向旧对象的引用修正到复制后的新地址,这个操作称为引用更新。引用更新的初始化阶段实际上并未做什么具体的处理,设立这个阶段只是为了建立一个线程集合点,确保所有并发回收阶段中进行的收集器线程都已完成分配给它们的对象移动任务而已。初始引用更新时间很短,会产生一个非常短暂的停顿。

  • 并发引用更新(Concurrent Update Reference):真正开始进行引用更新操作,这个阶段是与用户线程一起并发的,时间长短取决于内存中涉及的引用数量的多少。并发引用更新与并发标记不同,它不再需要沿着对象图来搜索,只需要按照内存物理地址的顺序,线性地搜索出引用类型,把旧值改为新值即可。

  • 最终引用更新(Final Update Reference):解决了堆中的引用更新后,还要修正存在于GC Roots中的引用,即前面修改的堆内存中对象的引用地址,现在要修改比如栈帧本地变量表的 reference。这个阶段是Shenandoah的最后一次停顿,停顿时间只与GC Roots的数量相关。

  • 并发清理(Concurrent Cleanup):经过并发回收和引用更新之后,整个回收集中所有的Region已再无存活对象,这些Region都变成Immediate Garbage Regions了,最后再调用一次并发清理过程来回收这些Region的内存空间,供以后新对象分配使用。

即最重要四个阶段:并发标记 -> 并发撤退存活对象 -> 并发引用更新 -> 并发回收

并发整理实现(即复制回收集存活对象后,如何并发更新内存中所有指向该对象的引用)

  • 转发指针

    • 原理:

      • 在原有对象布局结构的最前面统一增加一个新的引用字段,在正常不处于并发移动的情况下,该引用指向对象自己。
      • 即只需要修改旧对象上转发指针的引用位置,使其指向新对象,便可将所有对该对象的访问转发到新的副本上。这样只要旧对象的内存仍然存在,未被清理掉,虚拟机内存中所有通过旧引用地址访问的代码便仍然可用,都会被自动转发到新对象上继续工作。

      【JVM系列】低延迟回收器 Shenandoah

    • 缺点:

      • 每次对象访问会带来一次额外的转向开销,尽管这个开销已经被优化到只有一行汇编指令的程度 mov r13,QWORD PTR [r12+r14*8-0x8],不过,毕竟对象定位会被频繁使用到,这仍是一笔不可忽视的执行成本。
      • 每次复制一个对象都要浪费占用同样大小的内存,直到全部指向该对象的引用更新完毕。
    • 并发问题:

      • 并发读取:如果收集器线程与用户线程发生的只是并发读取那无论读到旧对象还是新对象上的字段,返回的结果都应该是一样的,这个场景还可以有一些“偷懒”的处理余地

      • 并发写入:但如果发生的是并发写入,就一定必须保证写操作只能发生在新复制的对象上,而不是写入旧对象的内存中。所以这里必须针对转发指针的访问操作采取同步施,让收集器线程或者用户线程对转发指针的访问只有其中之一能够成功,另外一个必须等待,避免两者交替进行。实际上Shenandoah收集器是通过比较并交换(Compare And Swap,CAS)操作来保证并发时对象的访问正确性的。

  • 读写屏障

    • 为了实现Brooks Pointer,Shenandoah在读、写屏障中都加入了额外的转发处理,因为每次读写都要转发,并不会说跟 ZGC 一样第一次访问后更新为新对象的引用。
    • 因为使用读屏障的代价,是比写屏障更大的(代码里对象读取的出现频率要比对象写入的频率高出很多,读屏障数量自然也要比写屏障多得多,所以读屏障的使用必须更加谨慎,不允许任何的重量级操作),所以计划在JDK 13中将Shenandoah的内存屏障模型改进为基于引用访问屏障(Load Reference Barrier) 的实现,只拦截对象中数据类型为引用类型的读写操作,而不去管原生数据类型等其他非引用字段的读写,这能够省去大量对原生类型、对象比较、对象加锁等场景中设置内存屏障所带来的消耗。

相关文章: