Shenandoah GC---低延迟垃圾收集器

950 阅读7分钟

相关概念

历史: Shenandoah GC是由 RedHat公司开发的的新型收集器,14年RedHat把Shenandoah贡献给了OpenJDK。
设计目标:实现一种能在任何堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的垃圾收集器。

与G1 GC的比较

相同点

  • 同样基于Region的堆内存布局,同样有着用于存放大对象Humongous Region
  • 默认的回收策略也同样是优先处理回收价值最大的Region

不同点

堆内存管理方面

  • 支持并发的整理算法,G1的回收阶段可以是并行的,但却不能与用户线程并发
  • 默认不使用分代收集,不会有专门的新生代Region或者老年代Region
  • 去除了G1中耗费内存和计算资源去维护的RSet,改为使用名为“连接矩阵”的全局数据结构来记录跨Region的引用,
    • 优点:
      • 降低了处理跨代指针时的记忆集维护消耗
      • 降低了伪共享的发生概率(伪共享相关内容:www.yuque.com/lihongjian/…
    • 连接矩阵: 可以理解为一张二维表格,如果Region N有对象指向Region M,就在表格的N行M列上打上一个标记**,**如果Region5中的对象Baz引用了Region3的Foo,Foo又引用了Region1 的Bar,那连接矩阵中的5行3列、3行1列就应该被打上标记,回收时通过这张表格就可以得出哪些Region之间产生了跨代引用。

用户态、核心态相关概念:zh.wikipedia.org/wiki/

运行过程

1、2、3阶段与G1相同

  1. 初始标记(STW)

标记所有GC Roots能直接关联到的对象,速度很快,会造成短暂的暂停,时间长短与堆内存的大小无关,与对象的多少无关,与存活对象多少也无关

  1. 并发标记

GC线程与用户线程并发执行,遍历整个对象图,标记出所有可达的对象,时间长短取决于存活对象的数量以及对象图的复杂度

  1. 最终标记(STW)

与G1一样处理剩余的SATB扫描,并在这个阶段统计出回收价值最高的Region,将这些Region构成一组回收集(CSet)

  1. 并发清理

将标记到没有对象存活的Region清理掉

  1. 并发回收
    1. 在CMS中会删除不再使用的对象,并回收他们占用的内存
    2. 在G1中的拷贝存活对象(evacuiation)阶段是筛选出部分Region后拷贝到新的Region中去,并将原有的Region清空。
    3. Shenandoah GC要把回收集里面的存活对象先复制一份到其他未被使用的Region中。
      1. **问题:由于与用户线程并发执行,用户线程会对对象不停的进行读写操作,但移动对象后整个内存中所有指向该对象的引用都需要改变,总不能把所有指向该对象的引用全都修改一遍,那属实是太麻烦了?**Shenandoah使用读屏障和被称为“Brooks Pointer”的转发指针来解决,下文会进行解释
  2. 初始引用更新
    1. 前一阶段已经复制完对象了,理论上要更新引用了是吧,但事实却不是这样的,这个阶段其实是建立一个线程的集合点,由于GC是多线程执行的,需要一个集合点来保证所有的GC线程都把自己负责的对象复制完了。
  3. 并发引用更新
    1. 真正开始进行引用更新操作,此阶段不需要和并发标记阶段一样遍历整个对象图来搜索,只需要按照内存屋里地址的顺序,线性地搜索出引用类型,把旧值改为新值即可(其实只是涉及修改被移动对象的对象头的转发指针的引用,下文会讲到),此阶段的时间长短与存活对象的个数有关
  4. 最终引用更新 (STW)
    1. 修正存在于GC Roots中的引用,停顿时间只与GC Roots数量有关,因为GC Roots不一定参与回收,因此需要更新对应的引用,否则下次GC时对象就不可达了
  5. 并发清理
    1. 经过上边的阶段,整个回收集中所有的Region中已经再无存活对象,这些Region都变成直接垃圾区,因此最后再调用一次并发清理过程来回收这些Region的内存空间,供以后新对象分配使用。

Brooks Pointer/转发指针

Brooks Pointer/转发指针旨在实现对象移动与用户程序并发的解决方案。

之前的处理方式

在这种概念出现之前,要做类似的并发操作,通常是在被移动对象原有的内存上设置保护陷阱(Memory Protection Trap),一旦用户程序访问到归属于旧对象的内存空间就会产生自陷中段,进入预设好的异常处理器中,再由其中的代码逻辑把访问转发到复制后的新对象上
缺点: 虽然能实现对象移动与用户线程并发, 但是如果没有操作系统层面的直接支持,这种方案将导致用户态频繁切换到内核态,代价是非常大的,不能频繁使用。

Brooks Pointer/转发指针的处理方案

在原有的对象布局结构的最前面统一增加一个新的引用字段,在正常不处于并发移动的情况下,该引用指向对象自己。
在这里插入图片描述
是不是与对象的访问方式中的“句柄”模式有点像,两者都是间接性的对象访问方式,差别是句柄通常会统一存储所在专门的句柄池中,而转发指针是分散存放在每一个对象头前面。

优点

当对象拥有了一份新的副本时(复制),只需要修改旧对象上转发指针的引用位置,使其指向新对象,便可以将所有对该对象的访问转发到新的副本上,只有旧对象的内存仍然存在,未被清理掉,虚拟机的内存中所有通过旧引用地址访问的代码便仍然可以使用。

缺点

与其他间接对象访问技术的缺点相同,每次访问对象都会带来一次额外的转向开销(尽管这个开销已经被优化到只有一行汇编指令的程度),由于对象访问会被频繁使用到,仍然是一笔不可忽视的执行成本,与内存保护陷阱的方案比起来好了很多。

多线程竞争问题


并发读: 当垃圾收集线程与用户线程并发读取对象内容时,无论是读到旧地址对象还是读到新地址对象,返回的内容都是一致的,因此并发读没问题。

并发写: 对对象的写操作无疑有如下三种
(1)收集器线程复制出了对象新的副本
(2)用户线程更新对象的某个字段
(3)收集器更新对象的转发指针的地址为新地址

想象一下当(1)执行了,(3)执行之前(2)来执行,是不是会出现用户线程的更新更新到了旧的对象上,此时旧的对象还没被清理,此时一切还是正常,但是等到旧对象被回收掉后,是不是就有问题了,因此必须保证对转发指针的同步,Shenandoah采用的是CAS的操作来保证的。

执行频率问题

Shenandoah通过设置读写屏障来处理转发指针,对象的读动作是很频繁的,远远大于写动作(前边介绍的GC都只是使用了写屏障,Shenandoah是第一款介绍的使用的读屏障的GC)

  • 读频率高,自然读屏障的个数要远大于写屏障
  • 在之前垃圾收集器需要的写屏障的地方又加了转发操作


计划在JDK13中将Shenandoah的内存屏障模型改进为基于引用访问屏障,即: 指内存屏障只拦截对象中数据类型为引用类型的读写操作,不管原生数据类型等其他非引用字段的读写,这能够省去大量对原生对象、对象比较、对象加锁等场景中设置内存屏障带来的消耗。