likes
comments
collection
share

一文详解Java高性能垃圾回收器ZGC,为什么比CMS、G1更快

作者站长头像
站长
· 阅读数 199

ZGC(Z Garbage Collector)是在JDK 11 中引入一种新的垃圾回收器,旨在提供非常低的暂停时间和高的吞吐量,尤其适用于超大堆内存的应用。是一个可伸缩的低延迟垃圾回收器。

CMS与G1的停顿时间

CMS(Concurrent Mark-Sweep)垃圾回收器在初始标记和重新标记阶段需要STW(Stop-The-World),G1(Garbage-First)垃圾回收器在初始标记、最终标记、部分复制清理阶段、回收阶段等都需要STW。在STW期间会暂停用户线程,增加系统响应时间,降低系统可用性。

ZGC的主要特点

  1. 极低的暂停时间
    • ZGC的设计目标是将GC暂停时间控制在10毫秒以下,即使在超大堆内存(多达数TB)的情况下也能保持低暂停时间。
  1. 并发收集
    • ZGC的大多数工作都是并发进行的,最大限度地减少应用线程的暂停时间。包括标记、重新标记和压缩等阶段,都是在应用线程运行的同时进行的。
  1. 可伸缩性
    • ZGC能够有效地处理非常大的堆内存,适用于需要高可用性和低延迟的大型应用。
  1. 基于染色指针(Colored Pointers)和读屏障(Load Barriers)
    • ZGC引入了染色指针(Colored Pointers)和读屏障(Load Barriers)技术,以便精确追踪和管理内存中的对象。这些技术允许ZGC在GC过程中安全地移动对象而不需要长时间的暂停。

ZGC回收过程

与CMS中的ParNew和G1类似,ZGC也采用标记-复制算法,不过ZGC对该算法做了重大改进:ZGC在标记、转移和重定位阶段几乎都是并发的,这是ZGC实现停顿时间小于10ms目标的最关键原因。

ZGC垃圾回收周期如下图所示:

一文详解Java高性能垃圾回收器ZGC,为什么比CMS、G1更快

ZGC只有三个STW阶段:初始标记,再标记,初始转移。其中,初始标记和初始转移分别都只需要扫描所有GC Roots,其处理时间和GC Roots的数量成正比,一般情况耗时非常短;再标记阶段STW时间很短,最多1ms,超过1ms则再次进入并发标记阶段。即,ZGC几乎所有暂停都只依赖于GC Roots集合大小,停顿时间不会随着堆的大小或者活跃对象的大小而增加。与ZGC对比,G1的转移阶段完全STW的,且停顿时间随存活对象的大小增加而增加。

ZGC关键技术

ZGC通过染色指针和读屏障技术,解决了转移过程中准确访问对象的问题,实现了并发转移。

大致原理描述如下:并发转移中“并发”意味着GC线程在转移对象的过程中,应用线程也在不停地访问对象。假设对象发生转移,但对象地址未及时更新,那么应用线程可能访问到旧地址,从而造成错误。而在ZGC中,应用线程访问对象将触发“读屏障”,如果发现对象被移动了,那么“读屏障”会把读出来的指针更新到对象的新地址上,这样应用线程始终访问的都是对象的新地址。那么,JVM是如何判断对象被移动过呢?就是利用对象引用的地址,即染色指针。下面介绍染色指针和读屏障技术细节。

染色指针(Colored Pointers)

ZGC使用染色指针来跟踪对象的状态,这种技术将状态信息存储对象指针中。染色指针包括以下几种状态:

  • Marked0:对象在当前并发标记周期内被标记为存活。
  • Marked1:对象在前一个并发标记周期内被标记为存活。
  • Remapped:对象已经被重定位。

这种技术减少了内存开销,并提高了对象状态管理的效率。

ZGC仅支持64位系统,它把64位虚拟地址空间划分为多个子空间,如下图所示:

一文详解Java高性能垃圾回收器ZGC,为什么比CMS、G1更快

其中,[0~4TB) 对应Java堆,[4TB ~ 8TB) 称为M0地址空间,[8TB ~ 12TB) 称为M1地址空间,[12TB ~ 16TB) 预留未使用,[16TB ~ 20TB) 称为Remapped空间。

当应用程序创建对象时,首先在堆空间申请一个虚拟地址,但该虚拟地址并不会映射到真正的物理地址。ZGC同时会为该对象在M0、M1和Remapped地址空间分别申请一个虚拟地址,且这三个虚拟地址对应同一个物理地址,但这三个空间在同一时间有且只有一个空间有效。ZGC之所以设置三个虚拟地址空间,是因为它使用“空间换时间”思想,去降低GC停顿时间。“空间换时间”中的空间是虚拟空间,而不是真正的物理空间。后续章节将详细介绍这三个空间的切换过程。

与上述地址空间划分相对应,ZGC实际仅使用64位地址空间的第041位,而第4245位存储元数据,第47~63位固定为0。

一文详解Java高性能垃圾回收器ZGC,为什么比CMS、G1更快

ZGC将对象存活信息存储在42~45位中,这与传统的垃圾回收并将对象存活信息放在对象头中完全不同。

读屏障(Load Barriers)

ZGC使用读屏障(Load Barriers)技术来处理对象的访问和引用更新。每次应用线程加载一个对象引用时,都会触发读屏障逻辑。读屏障检查对象的状态,并根据需要执行额外的处理,如更新对象引用或处理对象的重定位。

ZGC触发时机

相比于CMS和G1的GC触发机制,ZGC的GC触发机制有很大不同。ZGC的核心特点是并发,GC过程中一直有新的对象产生。如何保证在GC完成之前,新产生的对象不会将堆占满,是ZGC参数调优的第一大目标。因为在ZGC中,当垃圾来不及回收将堆占满时,会导致正在运行的线程停顿,持续时间可能长达秒级之久。

ZGC有多种GC触发机制,总结如下:

  • 阻塞内存分配请求触发: 当垃圾来不及回收,垃圾将堆占满时,会导致部分线程阻塞。我们应当避免出现这种触发方式。日志中关键字是“Allocation Stall”。
  • 基于分配速率的自适应算法: 最主要的GC触发方式,其算法原理可简单描述为”ZGC根据近期的对象分配速率以及GC时间,计算出当内存占用达到什么阈值时触发下一次GC”。自适应算法的详细理论可参考彭成寒《新一代垃圾回收器ZGC设计与实现》一书中的内容。通过ZAllocationSpikeTolerance参数控制阈值大小,该参数默认2,数值越大,越早的触发GC。我们通过调整此参数解决了一些问题。日志中关键字是“Allocation Rate”。
  • 基于固定时间间隔: 通过ZCollectionInterval控制,适合应对突增流量场景。流量平稳变化时,自适应算法可能在堆使用率达到95%以上才触发GC。流量突增时,自适应算法触发的时机可能会过晚,导致部分线程阻塞。我们通过调整此参数解决流量突增场景的问题,比如定时活动、秒杀等场景。日志中关键字是“Timer”。
  • 主动触发规则: 类似于固定间隔规则,但时间间隔不固定,是ZGC自行算出来的时机,我们的服务因为已经加了基于固定时间间隔的触发机制,所以通过-ZProactive参数将该功能关闭,以免GC频繁,影响服务可用性。 日志中关键字是“Proactive”。
  • 预热规则: 服务刚启动时出现,一般不需要关注。日志中关键字是“Warmup”。
  • 外部触发: 代码中显式调用System.gc()触发。 日志中关键字是“System.gc()”。
  • 元数据分配触发: 元数据区不足时导致,一般不需要关注。 日志中关键字是“Metadata GC Threshold”。

优势和局限性

优势

  1. 极低的暂停时间
    • ZGC的暂停时间通常在数毫秒以内,即使在超大堆内存情况下也能保持低暂停时间。
  1. 高可伸缩性
    • ZGC能够处理数TB级别的堆内存,适用于需要处理大量数据的大型应用。
  1. 并发处理
    • 大多数GC工作都并发进行,最大限度地减少了应用线程的停顿。

局限性

  1. 更高的CPU开销
    • 由于ZGC的大量工作是并发进行的,需要额外的CPU资源来处理读屏障和其他并发任务。
  1. 目前仅支持64位平台
    • ZGC目前只支持64位的Linux和macOS平台,不支持32位平台。
  1. 染色指针的局限性
    • 染色指针使用了对象引用的高位比特,这可能在某些情况下限制引用的使用。

配置与使用

ZGC(Z Garbage Collector)是一种旨在提供低延迟和高吞吐量的垃圾回收器。为了使ZGC在特定的应用场景中表现出最佳性能,我们可以通过一些常用的配置参数来调整其行为。以下是一些常用的ZGC配置参数及其解释:

启用ZGC

启用ZGC需要解锁实验性VM选项并指定使用ZGC:

-XX:+UnlockExperimentalVMOptions -XX:+UseZGC

设置堆内存大小

设置初始堆内存(最小堆内存)和最大堆内存:

-Xms<size>
-Xmx<size>

例如,将初始堆内存设置为8GB,最大堆内存设置为32GB:

-Xms8g -Xmx32g

设置暂停时间目标

ZGC允许设置目标最大暂停时间(单位为毫秒):

-XX:MaxGCPauseMillis=<time>

例如,设置目标最大暂停时间为10毫秒:

-XX:MaxGCPauseMillis=10

并发GC线程数

设置并发垃圾回收线程的数量。默认情况下,ZGC会根据CPU核心数自动调整:

-XX:ConcGCThreads=<threads>

例如,设置并发GC线程数为4:

-XX:ConcGCThreads=4

并发标记周期间隔

设置并发标记周期之间的时间间隔(默认是动态调整的,不常用但可根据需要调整):

-XX:ZMarkCycleInterval=<time>

使用大页(Huge Pages)

启用或禁用大页支持,大页可以减少内存管理开销:

-XX:+UseLargePages

堆内存的占用上限

设置堆内存的使用上限,以避免使用系统的全部内存:

-XX:ZUncommitDelay=<time>

打印GC日志

启用GC日志打印,帮助诊断和调优:

-Xlog:gc:gc.log

可以进一步控制日志详细程度:

-Xlog:gc*=debug:file=gc.log

内存限制

设置ZGC的最小和最大区域大小:

-XX:ZCollectionPeriod=<time>
-XX:ZFragmentationLimit=<percentage>
-XX:ZMarkStackSpaceLimit=<size>

其他调优参数

  • -XX:ZStatisticsInterval=1000:设置统计信息输出间隔(单位:毫秒)。
  • -XX:ZHighUsageThreshold=70:设置高内存使用率的阈值(单位:百分比)。
  • -XX:ZLowUsageThreshold=30:设置低内存使用率的阈值(单位:百分比)。

总结

ZGC是一种先进的低延迟垃圾回收器,适用于需要极低暂停时间和高可伸缩性的应用。尽管ZGC引入了复杂的技术,如染色指针和读屏障,但它提供了显著的性能优势,使其成为大规模Java应用的理想选择。

参考:tech.meituan.com/2020/08/06/…

转载自:https://juejin.cn/post/7378028569689833513
评论
请登录