ZGC 介绍

什么是 ZGC

由于现在系统日趋增长的内存,传统的垃圾回收器在整理阶段需要花费更长的时间,为了提高 jvm 在大容量内存应用的回收效率,一款新的垃圾回收器 ZGC 在 JDK11 上正式公布问世。通过配置参数 -XX:+UseZGC 开启,目前仅支持 Linux x86 64位的系统。

ZGC 全称 Z Garbage Collector,是一款 低停顿 的标记整理垃圾收集器,它能够在大部分时间与应用线程并行运行。ZGC 在 Oracle 官方资料中表明能够保证垃圾回收中最高 10毫秒 的停顿,而作为低停顿的代价也只是最多下降 15% 的总吞吐量。

参考资料: https://openjdk.java.net/projects/zgc/ https://www.youtube.com/watch?v=7k_XfLGu-Ts https://www.youtube.com/watch?v=kF_r3GE3zOo https://dinfuehr.github.io/blog/a-first-look-into-zgc/ https://www.opsian.com/blog/javas-new-zgc-is-very-exciting/ https://mp.weixin.qq.com/s/KUCs_BJUNfMMCO1T3_WAjw https://www.youtube.com/watch?v=tShc0dyFtgw&t=2007s https://www.youtube.com/watch?v=7cWiwu7kYkE http://likehui.top/2019/04/11/ZGC-%E7%89%B9%E6%80%A7%E8%A7%A3%E8%AF%BB/

gc-concurrent

特性

基于 Region 的并发收集模式

ZGC 与 G1 类似,也是以 Region 划分堆内存结构的,不同的是,ZGC 中 Region 是以大小进行分组的,分别是 Small (2MB)、Medium (32MB)、Large (N*MB),并且尚未进行分代。由于没有了分代,也就没有了 CardTableRememberedSet 的开销。

使用 NUMA架构技术高效的分配空间和进行对象的扫描

利用 NUMA 架构的 CPU 亲和的内存分配策略,在分配对象时使用线程所处的节点缓存,使线程在操作自身创建的对象时提高效率。

设计颜色指针标记对象状态,保障引用关系一致

颜色指针就好比状态机,ZGC 在对象地址的其中 4bit 的空间用于标记颜色状态,这四个字节分别称之 FinalizableRemappedMarked1Marked0,通过在不同的收集阶段对指定标记的状态检测,从而采取不同的执行动作。

由于需要额外的空间来存储标记,因此不支持 32 位平台和指针压缩。

  • Makred0、 Marked1 用于识别对象在垃圾回收周期中是否被标记存活,存在两个的原因是因为一个被标记的对象可能来自上一个回收周期并未重新映射,此类对象则只需进行映射而无需重定位。
  • Remapped 表明该引用对象需要从地址映射表中获取新地址并转移。
  • Finalizable 表示这是一个 finalizar 对象,只有 Finalizer 可以对其进行访问。

具体逻辑见 hotspot/share/gc/z/zAddress.hpphotspot/share/gc/z/zAddress.cpphotspot/share/gc/z/zAddress.inline.hpp

利用 读屏障 修改引用来提高对象的整理迁移功能

对比 G1 使用 写屏障 来保证引用关系一致,ZGC 则是使用 读屏障 来达到。

在应用线程与 GC 线程同时操作同一对象时,通过读屏障检测对象状态,通过 CAS 对重映射对象进行迁移。

这种设计使得无需暂停应用线程就能保证引用正确,而读屏障的性能开销只有约 4%。

触发策略

通过 hotspot/share/gc/z/zDirector.cpp 中可得知,ZGC 与其他 GC 的回收策略有所不同,是一种主动式的垃圾回收模式。

void ZDirector::run_service() {
  // Main loop
  while (_metronome.wait_for_tick()) {  // 1
    sample_allocation_rate();
    const GCCause::Cause cause = make_gc_decision(); // 2
    if (cause != GCCause::_no_gc) {
      ZCollectedHeap::heap()->collect(cause);
    }
  }
}
  1. wait_for_tick 中,JVM 会利用时钟计算等待时间,当到达唤醒时间并且不在安全点期间则执行后续逻辑。

  2. make_gc_decision 是根据条件返回回收策略,包含 4 种可执行垃圾回收的策略。

  • ** 定时执行 **

判断当前时间距离上次垃圾回收的差值,当时间差值大于设定的间隔时间时则触发垃圾回收。

  • ** 内存预热 **

根据堆内存使用率判断是否进行垃圾回收,当每突破一个 10% 值时进行垃圾回收,例如超过 20%、30% 时。

  • ** 吞吐量过大 **

对比剩余空间所需分配时间与最久 GC 时间差值,当大于指定间距时 (默认 0.1) 则说明存在空间不足分配的可能性,需要进行垃圾回收。

  • ** 主动触发 **

当距离上次垃圾回收已经过了 5 分钟同时堆空间上涨了 10%,并且距离上次 GC 的时间大于最久 GC 时间的 49 倍。

当这些条件满足时 JVM 将会调用 Monitor (synchronized 底层线程安全模块) 唤醒线程,异步 执行垃圾回收。

回收过程

ZGC 的回收过程几乎是完全并发进行的,只会在三个阶段进行短短的暂停: 标记开始(mark-start)、标记结束(mark-final)、迁移开始(relocate-start),具体流程可见于 src/hotspot/share/gc/z/zDriver.cpp::run_gc_cycle()

zgc-phases

  1. 【标记开始】 开始标记时,会暂停所有应用线程,标记出堆中的 GC Roots

mark-start

  1. 【并发标记】 释放应用线程,通过 GC Roots 遍历堆中所有对象,找出存活的对象集合 (类似一个 bitmap)。应用线程利用读屏障将对象的变化信息保存于线程中,之后转交由 GC 线程处理引用关系。

concurrent-mark

  1. 【标记结束】 完成所有对象的标记后,短时暂停应用线程,完成标记阶段。

mark-final

  1. 【并发准备】 为下一阶段的重定位做准备,收集那些垃圾对象占比最大或最多的 page 加入 relocate set,每个 page 都分配一个 forwarding table 保存重定向地址,还进行一些其他数据的清理 (比如软弱虚引用、Finalizer 对象、字符串常量池、元数据)。

prepare

  1. 【迁移开始】 暂停应用线程,扫描 GC Roots 的指向对象,对 relocate set 内对象进行迁移并将重定位地址写入 forwarding table。将本地线程状态设置为 bad mask,对应用线程内对迁移对象的引用标记为 Remapped relocate。分配大块连续空间,以便能够存放要迁移的对象,申请一下阶段所需的工作线程。

relocate-start

  1. 【并发迁移】relocate set 中 page 里剩余的存活对象进行迁移,将重定位地址写入 forwarding table 中。应用线程在操作 Remapped relocate 对象时将通过读屏障进行重新映射地址,同时 GC线程 也对程序内的对象引用进行重映射。当映射地址与原地址一致时则表明所有引用均已重映射,标记 page 在之后清除释放空间。

concurrent-relocate

  • 如未重映射对象在下一垃圾回收周期也被标记,则该对象则不会进行分配重定位地址,为了达到这一区别所以才有了 Makred0Marked1 两个标记标识。

停顿时间不会随着堆空间的大小增长,但是与 GC Root 的数量是成正比,而 GC Root 的数量则与应用线程的数量有关。 在 GC 的周期内,标记开始和迁移开始操作会比较花费时间,但所有停顿时间总共也是小于 10 毫秒。