JVM垃圾回收器学习3:ZGC

jvm,gc,垃圾回收器,zgc,java

Posted by Chris on December 1, 2019

1 背景

我们前面介绍过G1是一款比较优秀的垃圾回收器,但是,仍然存在一些缺点:

  • 对于内存很小的情况下不适用
    因为G1仍然采用分代收集的方式,且采用标记整理和复制算法,所以需要预留一部分内存用来做缓冲。这样,就导致G1需要比较多的内存才可以正常工作。
  • 仍然有可能发生Full GC
    当内存不够用时,G1无法正常工作,就会触发Full GC。即暂停用户线程,对于整个堆空间进行清理,这对于应用程序的影响很大。

为了解决G1的问题,Per Liden和Stefan Karlsson开发了一款更加优秀的垃圾回收器,即ZGC(Z Garbage Collector)。

2 ZGC的特点

ZGC的目标是:

  • GC的暂停时间不会超过10ms,且停顿时间不会随着堆内存或者存活对象的增加而增加
  • 可以适用于几百M到几个T的堆空间
  • 与使用G1相比,应用程序吞吐量降低不超过15%
  • 目前只适用于Linux 64位操作系统

ZGC的实现特点是:

  • 并发收集
  • 内存划分基于Region(ZPage)
  • 并没有使用分代收集
  • 会对堆内存进行整理
  • 使用NUMA高效的进行内存分配
  • 使用读屏障技术,在单个对象的粒度上,对于引用的变更进行记录
  • 使用着色指针标记对象的标记和迁移状态

最令人印象深刻的技术应该就是ZPage、NUMA、读屏障和着色指针了,下面先介绍这四个技术,然后介绍ZGC垃圾回收的过程。

3 ZGC实现细节

ZPage

与G1很相似,ZGC将堆空间划分成一个个Region,这些Region在ZGC中叫做ZPage。与G1不同的是:

  • ZPage的大小不是固定的,但一定是2的倍数。ZPage的大小有下面这些
    • Small (2 MB)
    • Medium (32 MB)
    • Large (N * 2 MB) 根据对象的大小,分配不同大小的ZPage。因此,ZGC的内存划分如下:
  • 真实的物理内存映射到一个更大的地址空间(可以达到4TB)上,因此,在地址空间里面寻找连续的地址空间就不是一个很大的问题了。
  • ZPage不区分新生代和老年代,省去了Remembered Set和CardTable的开销。

NUMA技术

NUMA,non-uniform memory access,是指CPU在访问靠近自己的内存时速度非常快,在访问其他CPU的内存时速度会相对较慢。因此,默认的内存分配策略就是:优先在线程所在的CPU对应的本地内存上分配内存。

如上图所示,当线程在CPU4上执行时,就会在Memonry 1上分配内存,这样分配内存和进行访问都很快。但是,这样做的话,CPU5上执行的线程想访问刚才分配的对象,就得通过interconnect这个网络来进行访问了,这样的速度相对较慢。

着色指针

指针(在java中称为引用)的地址为64bit,42bit用来代表对象的地址,ZGC使用剩下的22位中的4bit来表示ZGC有关的内容。这4个bit位代表的意思如下:

  • finalizable bit:指针所指向的对象只能通过finalizer指针来访问了。
  • remap bit:引用指向的地址已经更新过了,指向了对象的最新的地址。
  • remarked0remarked1 bits:用于标记可达对象。

单独使用着色指针,还无法保证访问该指针时,指针指向的内存地址会是最新的,得结合读屏障来使用。ZGC使用读屏障来保证并发标记或者并发迁移时,用户线程访问的对象的地址是最新的。

垃圾回收的过程

ZGC垃圾回收的过程如下图所示

共分为6个阶段:

  • 标记开始
    这个阶段会标记出被GC root指向的所有对象。这个阶段需要STW。
  • 并发标记
    这个阶段,GC线程遍历堆中的所有存活对象,并标记这些存活的对象。
  • 标记结束
    短时间暂停用户线程,完成一些特殊标记情况,例如弱引用的情况。

    标记开始、并发标记和标记结束这三个阶段,会用到引用中的marked0marked1这两个bit位。

  • 并发准备
    为下一阶段的重定位(relocate)做准备。该阶段会将可回收对象占比最大的ZPage,放入relocation set;此外,每个ZPage都分配一个forwarding table,保存了对象的新的地址。
  • 迁移开始
    relocation set的对象进行迁移,并将重迁移的地址放入forwarding table中。该阶段可能只会完成relocation set里部分对象的迁移工作。
  • 并发迁移
    上一阶段未完成的迁移工作,会在这个阶段继续做,直到relocation set中的对象被迁移完毕。这个阶段,很有可能出现用户线程访问relocaation set里的对象。此时,用户线程中的读屏障会发现该指针已经被标记为有新的地址,就会更新地址,然后返回这个新的对象。可以看出,一个引用被重迁移,有可能是GC线程做的,也有可能是用户线程做的。
    还可以看出,读屏障的引入,将重迁移的工作变成了“懒惰”迁移,即访问每个对象时再进行地址迁移,而不是集中对于这个Region的对象进行复制。“懒惰”迁移的引入,使得迁移工作分散开了,从而不需要很长时间的STW。

4 总结

本文简单的讲述了ZGC的特点、堆空间划分、着色指针和垃圾回收的过程,并没有太详细。如果想要更深入的了解,可以参考A FIRST LOOK INTO ZGC

5 参考

An Introduction to ZGC: A Scalable and Experimental Low-Latency JVM Garbage Collector
A FIRST LOOK INTO ZGC
ZGC 特性解读
Getting started with Z Garbage Collector (ZGC) in Java 11 [Tutorial]
ZGC 介绍
JEP 333: ZGC: A Scalable Low-Latency Garbage Collector (Experimental)