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/
特性
基于 Region 的并发收集模式
ZGC 与 G1 类似,也是以 Region 划分堆内存结构的,不同的是,ZGC 中 Region 是以大小进行分组的,分别是 Small (2MB)、Medium (32MB)、Large (N*MB),并且尚未进行分代。由于没有了分代,也就没有了 CardTable
和 RememberedSet
的开销。
使用 NUMA架构技术高效的分配空间和进行对象的扫描
利用 NUMA 架构的 CPU 亲和的内存分配策略,在分配对象时使用线程所处的节点缓存,使线程在操作自身创建的对象时提高效率。
设计颜色指针标记对象状态,保障引用关系一致
颜色指针就好比状态机,ZGC 在对象地址的其中 4bit 的空间用于标记颜色
状态,这四个字节分别称之 Finalizable
、 Remapped
、Marked1
、 Marked0
,通过在不同的收集阶段对指定标记的状态检测,从而采取不同的执行动作。
由于需要额外的空间来存储标记,因此不支持 32 位平台和指针压缩。
- Makred0、 Marked1 用于识别对象在垃圾回收周期中是否被标记存活,存在两个的原因是因为一个被标记的对象可能来自上一个回收周期并未重新映射,此类对象则只需进行映射而无需重定位。
- Remapped 表明该引用对象需要从地址映射表中获取新地址并转移。
- Finalizable 表示这是一个 finalizar 对象,只有 Finalizer 可以对其进行访问。
具体逻辑见 hotspot/share/gc/z/zAddress.hpp
、hotspot/share/gc/z/zAddress.cpp
、hotspot/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);
}
}
}
-
在
wait_for_tick
中,JVM 会利用时钟计算等待时间,当到达唤醒时间并且不在安全点期间则执行后续逻辑。 -
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()
。
- 【标记开始】 开始标记时,会暂停所有应用线程,标记出堆中的 GC Roots。
- 【并发标记】 释放应用线程,通过 GC Roots 遍历堆中所有对象,找出存活的对象集合 (类似一个 bitmap)。应用线程利用
读屏障
将对象的变化信息保存于线程中,之后转交由 GC 线程处理引用关系。
- 【标记结束】 完成所有对象的标记后,短时暂停应用线程,完成标记阶段。
- 【并发准备】 为下一阶段的重定位做准备,收集那些垃圾对象占比最大或最多的 page 加入
relocate set
,每个 page 都分配一个forwarding table
保存重定向地址,还进行一些其他数据的清理 (比如软弱虚引用、Finalizer 对象、字符串常量池、元数据)。
- 【迁移开始】 暂停应用线程,扫描 GC Roots 的指向对象,对
relocate set
内对象进行迁移并将重定位地址写入forwarding table
。将本地线程状态设置为bad mask
,对应用线程内对迁移对象的引用标记为Remapped relocate
。分配大块连续空间,以便能够存放要迁移的对象,申请一下阶段所需的工作线程。
- 【并发迁移】 对
relocate set
中 page 里剩余的存活对象进行迁移,将重定位地址写入forwarding table
中。应用线程在操作Remapped relocate
对象时将通过读屏障进行重新映射地址,同时 GC线程 也对程序内的对象引用进行重映射。当映射地址与原地址一致时则表明所有引用均已重映射,标记 page 在之后清除释放空间。
- 如未重映射对象在下一垃圾回收周期也被标记,则该对象则不会进行分配重定位地址,为了达到这一区别所以才有了
Makred0
、Marked1
两个标记标识。
停顿时间不会随着堆空间的大小增长,但是与 GC Root 的数量是成正比,而 GC Root 的数量则与应用线程的数量有关。 在 GC 的周期内,标记开始和迁移开始操作会比较花费时间,但所有停顿时间总共也是小于 10 毫秒。