Shenandoah GC 介绍

介绍

Shenandoah GC 是 OpenJDK 在 JDK12 推出的新一代 标记整理 的垃圾回收器,它的目标时为了使大容量内存应用在垃圾回收时只进行短暂的应用暂停,使用 -XX:+UseShenandoahGC 开启。

它能够在垃圾回收周期中进行 并发整理并发更新,不暂停应用线程,并将垃圾回收分解为多个阶段,来达到极短的暂停时间,同时不会随着堆内存大小而影响。

参考资料: https://wiki.openjdk.java.net/display/shenandoah/Main https://www.youtube.com/watch?v=VCeHkcwfF9Q https://www.youtube.com/watch?v=E1M3hNlhQCg https://www.researchgate.net/publication/306112816_Shenandoah_An_open-source_concurrent_compacting_garbage_collector_for_OpenJDK https://developers.redhat.com/blog/2019/06/27/shenandoah-gc-in-jdk-13-part-1-load-reference-barriers/ https://developers.redhat.com/blog/2019/06/28/shenandoah-gc-in-jdk-13-part-2-eliminating-the-forward-pointer-word/

特性

并发复制

Shenandoah GC 是如何做到并发复制的呢?这主要是利用了 读屏障Brooks Pointers

Brooks Pointers 是为了保证并发环境下数据一致性而设计的字段,它在对象地址中占用了 8 位地址来标识实际对象地址的偏移值。

通常情况下它指向当前对象地址自身,当进入并发整理的对象拷贝阶段时,GC 将会利用 CAS 操作替换新的地址偏移值,并保证只进行一次成功的操作。

当应用线程操作该对象时将会使用 读屏障 获取 brooks pointers 偏移值再计算得出实际地址返回。

hotspot/share/gc/shenandoah/shenandoahBrooksPointer.inline.hpp

inline oop ShenandoahBrooksPointer::forwardee(oop obj) {
  shenandoah_assert_correct(NULL, obj);
  return oop(*brooks_ptr_addr(obj));
}

inline HeapWord** ShenandoahBrooksPointer::brooks_ptr_addr(oop obj) {
  return (HeapWord**)((HeapWord*) obj + word_offset());
}

由于通常情况下并不需要使用读屏障来获取地址,Shenandoah GC 又引入了 slow path 的设计。

它的作用是,只有当垃圾回收进行中,并且对象处于 collection set 中,那么程序才会使用 读屏障 来获取对象地址。

同时 Shenandoah GC 还会对字节码进行优化,以确保程序正确的情况下减少用读屏障获取地址的使用。

concurrent-copy

由于需要使用额外的空间来存储 brooks pointers,则必须使用 64 位地址,同时堆空间也会增加 3% ~ 15% 的开销,而 CAS 和 读屏障 则产生 2% ~ 20% 的吞吐量下降。

空间划为

Shenandoah 与 G1 类似也是以 Region 来划分堆内存,但是不进行分代标记。

每个 Region 对应一个数组用来表示有哪些 Region 存在对象指向自身 Region 中的对象,这就避免了 G1 在年轻代之间的引用关系需要扫描整个年轻代甚至是堆的情况。

region-incoming

NUMA

与 ZGC 相同,利用 NUMA 架构的 CPU 亲和的内存分配策略,在分配对象时使用线程所处的 CPU 缓存,并且使 应用线程 与 GC线程 处于同一核心线程下,共享同一个 Region 的数据。

触发策略

hotspot/share/gc/shenandoah/shenandoahControlThread.cpp::run_service() 可以观察到 Shenandoah GC 的垃圾收回触发策略,将会根据 内存分配情况 或 提交的垃圾回收事件 _requested_gc_cause 来决定所执行的具体垃圾回收流程。

  switch (mode) {
    case none:
      break;
    case concurrent_traversal:
      service_concurrent_traversal_cycle(cause);
      break;
    case concurrent_normal:
      service_concurrent_normal_cycle(cause);
      break;
    case stw_degenerated:
      service_stw_degenerated_cycle(cause, degen_point);
      break;
    case stw_full:
      service_stw_full_cycle(cause);
      break;
    default:
      ShouldNotReachHere();
  }

回收过程

Shenandoah 的回收过程与 G1 类似,都是基于 Region 的收集策略,一般情况下将垃圾回收声明周期分解成数个阶段执行。

shenandoah-gc-cycle

  1. 初始标记

暂停应用线程,扫描 GC Roots。

  1. 并发标记

与应用程序并发进行,通过上一阶段的 GC Roots 遍历堆,使用 STAB 算法描绘存活对象图谱。

  1. 最终标记

暂停应用线程,处理位于队列中的标记和更新操作,并预先进行下一阶段,可能与下一阶段同时完成。

收集存在死亡对象的 Region

  1. 并发清理

将无存活对象的 Region 进行清理并加入空闲列表。

  1. 并发疏散

将收集集合中的对象复制至空闲 Region 中,此阶段不会造成应用线程暂停。

  1. 初始更新引用

短暂的暂停应用线程,只确保疏散操作均已完成。

  1. 并发更新引用

遍历堆中对象,更新疏散对象的引用。

  1. 最终更新引用

暂停应用线程,堆当前的 Root Set 进行更新,对收集集合的 Region 进行回收。

  1. 并发清理

回收无任何对象引用的 Region。