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:引用指向的地址已经更新过了,指向了对象的最新的地址。
- remarked0和remarked1 bits:用于标记可达对象。
单独使用着色指针,还无法保证访问该指针时,指针指向的内存地址会是最新的,得结合读屏障来使用。ZGC使用读屏障来保证并发标记或者并发迁移时,用户线程访问的对象的地址是最新的。
垃圾回收的过程
ZGC垃圾回收的过程如下图所示

共分为6个阶段:
- 标记开始
这个阶段会标记出被GC root指向的所有对象。这个阶段需要STW。 - 并发标记
这个阶段,GC线程遍历堆中的所有存活对象,并标记这些存活的对象。 -
标记结束
短时间暂停用户线程,完成一些特殊标记情况,例如弱引用的情况。标记开始、并发标记和标记结束这三个阶段,会用到引用中的
marked0和marked1这两个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)