极致低延迟收集器ZGC探索——亚毫秒级,常数级暂停O(1)原理

news/2024/5/20 0:01:30/文章来源:https://blog.csdn.net/u011863951/article/details/129878985

ZGC 收集器

ZGC收集器(Z Garbage Collector)是由Oracle公司为HotSpot JDK研发的,最新一代垃圾收集器。有说法使用这个名目标是取代之前的大部分垃圾收集器,所以才叫ZGC,表示极致的Extremely,或者最后的,垃圾收集器。类似 ZFS(文件系统),ZFS(文件系统)在它刚问世时在许多方面都是革命性的。

ZGC官网

但是ZGC官方文档说ZGC这只是个名字,不代表任何含义。看你相信哪种了,笑

  • 设计目标
    希望能在尽可能对吞吐量影响不太大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在10ms以内的低延迟。
    • 停顿时间不超过10ms;
    • 停顿时间不会随着堆的大小,或者活跃对象的大小而增加;
    • 支持8MB~4TB级别的堆(未来支持16TB)。

主流的常见操作系统,比如Linux,Windows,MacOS,FreeBSD都是非实时操作系统。非实时操作系统的一个处理器时间片都在5~20毫秒,面向服务端的系统一个线程调度事件需要3-5个时间片,客户端系统则更多。10毫秒停顿已经可以认为是系统误差级的停顿,此时ZGC基本已经成为无停顿GC。

ZGC设计目标停顿时间在10ms以下,10ms其实是一个很保守的数据,在SPECjbb 2015基准测试中,128G的大堆下最大停顿时间才1.68ms,远低于10ms。
而且ZGC目前的进展很快,在JDK17的测试中和shenandoah gc双双实现了亚毫秒(<1ms)的GC暂停。
Shenandoah in OpenJDK 17: Sub-millisecond GC pauses | Red Hat Developer
不负极致之名,Java17之后采用ZGC是最好的选择。

ZGC历程

在Java11推出实验性的ZGC以来,历经数年开发,ZGC在当前已经新增了众多特性。
一些关于ZGC特性、原理的文章已经稍有过时,比如ZGC只支持4TB大小的堆,ZGC不支持类卸载,ZGC只支持Linux/x64架构等。

不过通过这些文章对ZGC进行了解还是可行的。

ZGC各版本特性变化

  • JDK 12
    • 支持并发类卸载
  • JDK 13
    • 支持最大堆从4TB提升到16TB
    • 支持Linux/AArch64架构
    • 支持归还未使用内存
  • JDK 14
    • 支持MacOS/x64、Windows/x64架构
    • 支持最低8M的小堆
  • JDK 15
    • 生产就绪
    • 支持类数据共享(CDS)
    • 支持压缩类指针(对象头)
    • 支持渐进式归还内存
  • JDK 16
    • 新增并发线程栈扫描特性
    • 支持对象就地迁移
    • 支持Windows/aarch64架构
  • JDK 17
    • 新增动态GC线程数特性
    • 新增JVM快速退出特性
    • 支持macOS/aarch64架构
  • JDK 18
    • 支持字符串重复数据删除
    • 支持Linux/PowerPC架构

ZGC 特性

  • 完全并发
  • 使用着色指针
  • 使用读屏障
  • 基于区块的内存模型
  • 支持就近分配的NUMA处理器架构
  • 压缩内存

其中完全并发的能力,是通过着色指针,读屏障,基于区块的内存模型来实现的,算是ZGC的基础特性。后面会首先研究。
支持就近分配的NUMA处理器架构,压缩内存等是性能提升措施,会在完全并发之后介绍。

基于区块的内存模型

类似于G1,ZGC也采用基于区块(Region)的堆内存布局,每个区块被称为ZPage。不同于G1的是,ZGC的区块具有动态性。ZGC的区块,支持动态创建和销毁,支持动态的区域容量大小变化。
ZGC区块分为以下几种

  • 小型区块(Small Region):
    容量固定为2MB,用于放置小于256KB的小对象。

  • 中型区块(Medium Region):
    容量固定为32MB,用于放置大于等于256KB但小于4MB的对象。

  • 大型区块(Large Region):
    容量不固定,可以动态变化,但必须为2MB的整数倍,用于放置4MB或以上的大对象。每个大型Region中只会存放一个大对象,所以实际容量可能小于中型Region,最小容量可低至4MB。大型Region在ZGC的实现中是不会被重分配的,因为复制一个大对象的代价非常高昂。

ZPage
可以看到相比G1,ZGC的区块动态性不包括堆内存的每个区块可以根据运行情况的需要,扮演年轻代的Eden、Survivor区域、老年代区域、或者大对象(Humongous)区域。这是因为ZGC目前并不支持分代垃圾回收,没错,ZGC这个强大的收集器目前并不支持分代,据说是因为实现分代太复杂了,连Oracle团队也比较棘手。但不代表ZGC就不会用分代模型,已经有让ZGC支持分代回收的提案了JEP439,就看未来什么时候能实现。

完全并发原理

ZGC的最大特性就是做到了GC过程中的大部分阶段都能和用户线程并发,只有极少阶段(<1ms)需要停顿,那么ZGC是如何做到的呢?

G1的回收时停顿

G1和ZGC都基于标记-复制算法,但算法具体实现的不同就导致了巨大的性能差异。

以G1为例,通过G1中标记-复制算法过程(G1的Young GC和Mixed GC均采用该算法),分析G1的混合回收中停顿耗时的主要瓶颈。
G1中标记-复制算法过程
已知G1混合回收采用了标记—复制的算法,混合回收(MixedGC)过程可以分为标记阶段、筛选回收阶段。其中筛选回收又分为清理阶段和复制阶段。

  • 标记阶段停顿分析 耗时较短
    • 初始标记阶段:初始标记阶段是指从GC Roots出发标记全部直接子节点的过程,该阶段是STW的。由于GC Roots数量不多,通常该阶段耗时非常短。
    • 并发标记阶段:并发标记阶段是指从GC Roots开始对堆中对象进行可达性分析,找出存活对象。该阶段是并发的,即应用线程和GC线程可以同时活动。并发标记耗时相对长很多,但因为不是程序停顿,所以我们不太关心该阶段耗时的长短。
    • 再标记阶段:重新标记那些在并发标记阶段发生变化的对象。该阶段是STW的。
  • 清理阶段停顿分析 耗时较短
    清理阶段识别出有存活对象的分区和没有存活对象的分区,该阶段不会清理垃圾对象,也不会执行存活对象的复制。该阶段是程序停顿的。
  • 复制阶段停顿分析 耗时较长
    复制算法中的转移阶段需要分配新内存和复制对象的成员变量。转移阶段是程序停顿的,其中内存分配通常耗时非常短,但对象成员变量的复制耗时有可能较长,这是因为复制耗时与存活对象数量与对象复杂度成正比。对象越复杂,复制耗时越长。

四个STW过程中,初始标记因为只标记GC Roots,耗时较短。再标记因为对象数少,耗时也较短。清理阶段因为内存分区数量少,耗时也较短。 复制-转移阶段要处理所有存活的对象,耗时会较长。因此,G1停顿时间的瓶颈主要是标记-复制算法中的复制-转移阶段的程序停顿 。为什么转移阶段不能和标记阶段一样并发执行呢?主要是G1未能解决转移过程中准确定位对象地址的问题。

ZGC的标记—复制算法

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

 ZGC的标记—复制算法
ZGC中的一次垃圾回收过程会被分为十个步骤:初始标记、并发标记、再次标记、并发转移准备:[非强引用并发标记、重置转移集、回收无效页面(区)、选择目标回收页面、初始化转移集(表)]、初始转移、并发转移。但是只有三个阶段需要停顿(STW):初始标记,再标记,初始转移。其中,初始标记和初始转移分别都只需要扫描所有GC Roots,其处理时间和GC Roots的数量成正比,一般情况耗时非常短;再标记阶段STW时间很短,最多1ms,超过1ms则再次进入并发标记阶段。即,ZGC几乎所有暂停都只依赖于GC Roots集合大小,停顿时间不会随着堆的大小或者活跃对象的大小而增加。与ZGC对比,G1的转移阶段完全STW的,且停顿时间随存活对象的大小增加而增加。

  • ①初始标记
    这个阶段会触发STW,仅标记根可直达的对象,并将其压入到标记栈中,在该阶段中也会发生一些其他动作,如重置 TLAB、判断是否要清除软引用等。
  • ②并发标记
    根据「初始标记」的根对象开启多条GC线程,并发遍历对象图,同时也会统计每个分区/页面中的存活对象数量。
  • ③再次标记
    这个阶段也会出现短暂的STW,因为「并发标记」阶段中应用线程还是在运行的,所以会修改对象的引用导致漏标的情况出现,因此需要再次标记阶段来标记漏标的对象(如果此阶段停顿时间过长,ZGC会再次进入并发标记阶段重新标记)。
  • 并发转移准备
    4~8阶段都是并发转移对象的准备阶段,各子阶段又分别处理了不同事务
    • ④非强引用并发标记和引用并发处理
      遍历前面过程中的非强引用类型根对象,但并不是所有非强根对象都可并发标记,有部分不能并发标记的非强根对象会在前面的「再次标记」阶段中处理。同时也会标记堆中的非强引用类型对象。
    • ⑤重置转移集/表
      重置上一次GC发生时,转移表中记录的数据,方便本次GC使用。
      在ZGC中,因为在回收时需要把一个分区中的存活对象转移进另外一个空闲分区中,而ZGC的转移又是并发执行的,因此,一条用户线程访问堆中的一个对象时,该对象恰巧被转移了,那么这条用户线程根据原本的指针是无法定位对象的,所以在ZGC中引入了转移表forwardingTable的概念。
      转移表可以理解为一个Map<OldAddress,NewAddress>结构的集合,当一条线程根据指针访问一个被转移的对象时,如果该对象已经被转移,则会根据转移表的记录去新地址中查找对象,并同时会更新指针的引用。
    • ⑥回收无效分区/页面
      回收物理内存已经被释放的无效的虚拟内存页面。ZGC是一款支持返还堆内存给物理机器的收集器,在机器内存紧张时会释放一些未使用的堆空间,但释放的页面需要在新一轮标记完成之后才能释放,所以在这个阶段其实回收的是上一次GC释放的空间。
    • ⑦选择待回收的分区/页面
      ZGC与G1收集器一样,也会存在「垃圾优先」的特性,在标记完成后,整个堆中会有很多分区可以回收,ZGC也会筛选出回收价值最大的页面来作为本次GC回收的目标。
    • ⑧初始化待转移集合的转移表
      初始化待回收分区/页面的转移表,方便记录区中存活对象的转移信息。
      注:每个页面/分区都存在一个转移表forwardingTable
  • ⑨初始转移
    这个阶段会发生STW,遍历所有GCRoots节点及其直连对象,如果遍历到的对象在回收分区集合内,则在新的分区中为该对象分配对应的空间。不过值得注意的是:该阶段只会转移根对象(也就是GCRoots节点直连对象)。
  • ⑩并发转移
    这个阶段与之前的「并发标记」很相似,从上一步转移的根对象出发,遍历目标区域中的所有对象,做并发转移处理。

ZGC对象定位

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

着色指针 Color Pointer

已知Java虚拟机垃圾回收时的可达性分析使用了标记-整理类算法。从垃圾回收扫描根集合开始标记存活对象,那么这些标记被储存在哪里?

HotSpot虚拟机的标记实现方案有如下几种

  • 把标记直接记录在对象头上
    如Serial收集器
  • 把标记记录在与对象相互独立的数据结构上
    如G1、Shenandoah使用了一种相当于堆内存的1/64大小的,称为BitMap的结构来记录标记信息
  • 直接把标记信息记在引用对象的指针上
    如ZGC

为什么可以把引用关系放在指针上?

可达性分析算法的标记阶段就是看有没有引用,所以可以只和指针打交道而不管指针所引用的对象本身。
例如使用三色标记法标记对象是否可达,这些标记本质上只和对象引用有关,和对象本身无关。只有对象的引用关系才能决定它的存活。

着色指针是一种直接将少量额外的信息存储在对象指针上的技术。目前在X64架构的操作系统中高16位是不能用来寻址的。程序只能使用低48位,
ZGC将低48位中的高4位取出,用来存储4个标志位。剩余的44位可以支持16TB(2的44次幂)的内存,也即ZGC可以管理的内存不超过16TB。
4个标志位即着色位,所以这种指针被称为着色指针。
在ZGC中标记信息被直接记在引用对象的着色指针上,这样通过对象着色指针就可以获取 GC 标记,解决转移过程中准确定位对象地址的问题。

因此,ZGC只能在64位系统上,因为ZGC的着色指针使用的是44-48位,32位的x86架构系统显然不支持,并且因为ZGC已经把48位可用的指针地址空间全部使用了,自然也不支持压缩指针。
着色指针

压缩指针和压缩类指针是两个不同的特性,后者又叫压缩对象头,ZGC是支持压缩对象头这一特性的,在JDK15后提供。

着色位
ZGC的四个着色位可以记录四种垃圾回收标记状态,即marked0、marked1、remapped、Finalizable,好像给指针染上了四种不同的颜色,所以叫做着色指针。

  • 指针如何实现染色
    指针的原本的作用在于寻址,如果我们想实现染色指针,就得把43~46位赋予特殊含义,这样寻址就不对了。所以最简单的方式是寻址之前把指针进行裁剪,只使用低44位去寻址(最大16TB内存)这样做导致的问题是,需要将裁剪指针寻址地址的 CPU 指令添加到生成的代码中,会导致应用程序变慢。
    为了解决上面指针裁剪的问题,ZGC 使用了mmap内核函数进行多虚拟地址内存映射。使用 mmap 可以将同一块物理内存映射到多个虚拟地址上。这样,就可以实现堆中的一个对象,有4个虚拟地址,不同的地址标记不同的状态 marked0、marked1、remapped,Finalizable。且都可以访问到内存。这样实现了指针染色的目的,且不用对指针进行裁剪,提高了效率。

着色指针的四个着色状态

  • Finalizable=1000 终结状态
    表示对象已经要被回收了,此位与并发引用处理有关,表示这个对象只能通过finalizer才能访问。
  • Remapped=0100 未扫描状态
    设置此位的值后,表示这个对象未指向RelocationSet中(relocation set表示需要GC的Region分区/页面集合)。
  • Marked1=0010 已标记状态
    对象已标记状态,用于辅助GC。
  • Marked0=0001 已标记状态
    对象已标记状态,用于辅助GC。

为什么会有两个Marked标识
这是为了防止不同GC周期之间的标记混淆,所以搞了两个Marked标识,每当新的一次GC开始时,都会交换使用的标记位。例如:第一次GC使用M0,第二次GC就会使用M1,第三次又会使用M0…,因为ZGC标记完所有分区的存活对象后,会选择分区进行回收,因此有一部分区域内的存活对象不会被转移,那么这些对象的标识就不会复位,会停留在之前的Marked标识(比如M0),如果下次GC还是使用相同M0来标记对象,那混淆了这两种对象。为了确保标记不会混淆,所以搞了两个Marked标识交替使用。

内存视图 View

内存视图是指ZGC对Java对象状态的一种描述,通过内存视图和着色指针配合,ZGC得以完成在并发转移对象的同时准确定位对象地址
ZGC将所有对象划分为 3 种不同的视图(状态):marked0、marked1、remapped,同一时刻只能处于其中一种视图(状态)。比如:

  • 在没有进行垃圾回收时,视图为remapped

  • 在 GC 进行标记开始,将视图从 remapped 切换到marked0/marked1

  • 在 GC 进行转移阶段,又将视图从marked0/marked1 切换到remapped

  • “好”指针和“坏”指针
    任意线程当前访问对象的指针的着色状态和当前所处的视图一致时,则当前指针为** “好”指针** ;当前指访问对象的指针的状态和当前所处的视图不一致时,则为**“坏”指针**。
    线程访问到好指针无需处理,直接通过指针访问对象地址。

  • 触发读屏障
    线程访问到坏指针时,在不同阶段会有不同的处理,处理过程在读屏障中实现。

    • 标记阶段
      访问到坏指针时,说明此对象存活且未被标记,会将指针着色状态调整为已标记的M0/M1状态。
    • 转移阶段
      访问到坏指针时,说明此对象需要被移动。线程会转移对象,然后将指针着色状态调整为未标记的Remapped状态,等待下轮GC扫描。
  • 着色指针的**“自愈”**
    通过上面的说明,发现线程访问到坏指针,在触发读屏障处理后,又恢复成好指针,且直到下轮GC之间无需再处理,线程可以直接访问对象。这一特性被称之为,ZGC的指针拥有“自愈”能力。

读屏障 Load Barrier

读屏障是一小段在特殊位置由 JIT 注入的代码,类似我们 JAVA 中常用的 AOP 技术;主要目的是处理GC并发转移后地址定位问题,对象漏标问题。

Object o = obj.fieldA; // 只有从堆中获取一个对象时,才会触发读屏障//读屏障伪代码
if (!(o & good_bit_mask)) {if (o != null) {//处理并注册地址slow_path(register_for(o), address_of(obj.fieldA));       }}
  • 处理对象漏标问题
    读屏障是在读取成员变量时,统统记录下来,这种做法是保守的,但也是安全的。根据三色标记法,引发漏标问题必须要满足两个条件,其中条件二为:「已经标为黑色的对象重新与白色对象建立了引用关系」,也就是已经标记过的存活对象(黑色对象)重新和垃圾对象(白色对象)建立了引用关系,而黑色对象想要与白色对象重新建立引用的前提是:得先读取到白色对象,此时读屏障的作用就出来了,可以直接记录谁读取了当前白色对象,然后在「再次标记」重新标记一下这些黑色对象即可。

  • 处理并发转移时对象地址定位问题
    GC发生后,堆中一部分存活对象被转移,当应用线程读取对象时,可以利用读屏障通过指针上的标志来判断对象是否被转移,如果读取的对象已经被转移(线程读取到坏指针),那么则修正当前对象引用为最新地址(去转移表中查)。这样做的好处在于:下次其他线程再读取该转移对象时,可以正常访问读取到最新值(着色指针的自愈)。

转移表 Forwarding Table

转移表ForwardingTable是ZGC确保转移对象后,其他引用指针能够指向最新地址的一种技术,每个页面/分区(ZPage)中都会存在,其实就是该区中所有存活对象的转移记录,也称之为「活跃信息表」。一条线程通过引用来读取对象时,发现对象被转移后就会去转移表中查询最新的地址,并更新地址。这样在并发场景下,用户线程使用读屏障就可以通过转发表拿到新地址,用户线程可以准确访问并发转移阶段的对象了。
转移表中的数据会在发生下一次GC时清空重置,也包括会在下一次GC时触发着色指针的重映射/重定位操作。在下一次GC并发标记阶段会遍历转发表,完成所有的地址转发过程,最后在并发转移准备阶段会清空转发表。
转移表

并发标记过程

ZGC基于染色指针的并发处理过程:

  • 在第一次GC发生前,堆中所有对象的标识为:Remapped 初始状态。
  • 第一次GC被触发后,此时内存视图已经为开始GC的M0状态。GC线程开始标记,开始扫描,如果对象是Remapped标志,并且该对象根节点可达的,则将其改为M0标识,表示存活对象且已被标记。
  • 如果标记过程中,扫描到的对象标识已经为M0,代表该对象已经被标记过,或者是GC开始后新分配的对象,这种情况下无需处理。
  • 在GC开始后,用户线程新创建的对象,会直接标识为和内存视图一致的M0状态。
  • 在标记阶段,GC线程仅标记用户线程可直接访问的对象还是不够的,实际上还需要把对象的成员变量所引用的对象都进行递归标记。

在「标记阶段」结束后,对象要么是M0存活状态,要么是未被标记的Remapped初始状态,说明这些对象不可达,即待回收状态。最终,所有被标记为M0状态的活跃对象都会被放入「活跃信息表」中。等到了「转移阶段」再对这些对象进行处理,流程如下:

  • ZGC选择目标回收区域,开始并发转移,此时内存视图切换为Remapped状态。
  • GC线程遍历访问目标区域中的对象,如果对象标识为M0并且存在于活跃表中,则把该对象转移到新的分区/页面空间中,同时将其标识修正为Remapped标志。
  • GC线程如果扫描到的对象存在于活跃表中,但标识为Remapped,说明该对象已经转移过了,无需处理。
  • 用户线程在「转移阶段」新创建的对象,会被标识为和内存视图一致的Remapped状态。
  • 如果GC线程遍历到的对象不是M0状态或不在活跃表中,说明不可达,也无需转移处理。
    最终,当目标区域中的所有存活对象被转移到新的分区后,ZGC统一回收原本的选择的回收区域。至此,一轮GC结束,整个堆空间会正常执行应用任务,直至触发下一轮GC。而当下一轮GC发生时,会采用M1作为GC辅助标识,而并非M0,具体原因在前面分析过了则不再阐述。

ZGC 其他特性

支持非统一内存访问架构

UMA架构:UMA即Uniform Memory Access Architecture(统一内存访问),UMA也就是一般正常电脑的常用架构,一块内存多颗CPU,所有CPU在处理时都去访问一块内存,所以必然就会出现竞争(争夺内存主线访问权),而操作系统为了避免竞争过程中出现安全性问题,注定着也会伴随锁概念存在,有锁在的场景定然就会影响效率。同时CPU访问内存都需要通过总线和北桥,因此当CPU核数越来越多时,渐渐的总线和北桥就成为瓶颈,从而导致使用UMA/SMP架构机器CPU核数越多,竞争会越大,性能会越低。

NUMA架构:NUMA即Non Uniform Memory Access Architecture(非统一内存访问),NUMA架构下,每颗CPU都会对应有一块内存,具体内存取决于处理器的内存位置,一般与CPU对应的内存都是在主板上离该CPU最近的,CPU会优先访问这块内存,每颗CPU各自访问距离自己最近的内存,效率自然而然就提高了。
NUMA架构允许多台机器共同组成一个服务供给外部使用,NUMA技术可以使众多服务器像单一系统那样运转,该架构在中大型系统上一直非常盛行,也是高性能的解决方案,尤其在系统延迟方面表现都很优秀,因此,实际上堆空间也可以由多台机器的内存组成。

通过NUMA非统一内存访问架构,机器得以纵向扩展,硬件性能堆叠,提供TB级内存单元。
ZGC是能自动感知处理器是否是NUMA架构,并可以充分利用NUMA架构特性的一款垃圾收集器。
ZGC在NUMA架构的处理器上,为活跃线程分配对象时,会就近分配到此线程所在处理器的优先访问内存上。

栈水印屏障

JDK16后通过JEP 376提案合入JDK主线,ZGC的又一强大特性。有了这一特性的支持,从JDK 16开始。ZGC现在的暂停时间为O(1)。换句话说,它们是在恒定的时间内执行的,并且不会随着堆、活动对象集或GC Roots根集大小(或其他任何东西)的增加而增加。

栈水印屏障是什么?先看一下官方博客对此的说明

在JDK 16之前,ZGC的暂停时间仍然随GC Roots根集的大小(子集)而缩放。更准确地说,ZGC仍然在停止世界阶段扫描线程栈。这意味着,如果一个Java应用有大量的线程,那么暂停时间会增加。如果这些线程有很深的调用栈,那么暂停时间会增加得更多。从JDK 16开始,线程栈的扫描是并发进行的,即在Java应用程序继续运行的同时进行。
栈水印屏障机制,可以防止Java线程在没有首先检查是否可以安全返回的情况下返回到栈帧。可以把它看作是栈帧的读屏障,如果需要的话,它将迫使Java线程在返回到栈帧之前采取某种形式的动作,使栈帧进入安全状态。每个 Java 线程都有一个或多个栈水印屏障,它告诉屏障在没有任何特殊操作的情况下可以在栈中安全地走多远。要走过一个水印,就要走一条慢路,使一个或多个栈帧进入当前安全状态,并更新水印。将所有线程栈带入安全状态的工作通常由一个或多个GC线程处理,但由于这是并发完成的,如果Java线程返回到GC线程尚未到达的栈帧中,有时就必须修复自己的几个栈帧。
有了JEP 376,ZGC现在在Stop-The-World阶段扫描的根数正好为零。

虽然说的有些绕,但还是说明了问题和解决方案。

问题就是,在JDK 16之前,ZGC的暂停时间仍然随GC Roots根集的大小增大而增大,因为ZGC要在程序完全停顿时,去扫描每个线程的所有栈帧中的回收根GCRoots。因为线程一旦运行,回收根很可能就会变化么,这很好理解。
所以随着线程增多等原因,GC Roots根集增大,那自然停顿时间也要增加了呗。

ZGC对这个问题怎么处理的?读屏障,没错还是读屏障,这次是对于线程栈帧使用的读屏障。

就地重定位

对比Azul Zing C4 GC

C4收集器由Azul的无暂停垃圾收集器PauseLessGC发展而来,相比PauseLess收集器,C4收集器最大的改进就是支持了分代回收模型。
这有点像ZGC的发展历程,目前(截止JDK18)的ZGC都是不支持分代的,而支持分代的ZGC正在开发中。
有观点认为ZGC就是重写的,纯软件实现的Azul PauseLessGC。目前正在追逐接近C4GC的目标。

C4全名 Continuously Concurrent Compacting Collector,连续并发压缩回收器。

ZGC的完全并发能力,对应C4的 Continuously Concurrent 连续并发能力
ZGC的标记—整理算法,就地重定位能力,对应C4的 Compacting 压缩能力
现在也就差分代回收未实现了。
没有分代回收,ZGC在极高对象分配速率时,仍然不及C4GC。

总结

ZGC 优点

  • 低停顿,高吞吐量,ZGC收集过程中额外耗费的内存小。
    • 低停顿,几乎所有过程都是并发的,只有短暂的STW。
    • 占用额外的内存小。G1通过写屏障维护记忆集,才能处理跨代指针,得以实现增量回收。记忆集占用大量内存,写屏障对正常程序造成额外负担。而ZGC没有写屏障,卡表之类的。(但这主要得益于ZGC目前没有实现分代回收,要是分代回收实现之后,还会不会这样不好说了)
    • 吞吐量方面,在ZGC的‘弱项’吞吐量方面,因为和用户线程并发,还是有影响的。但是以低延迟为首要目标的ZGC已经达到了以高吞吐量为目标Parallel Scavenge收集器的99%,直接超越了G1
  • 支持NUMA架构
    现在多CPU插槽的服务器都是NUMA架构,比如两颗CPU插槽(24核),64G内存的服务器,那其中一颗CPU上的12个核,访问从属于它的32G本地内存,要比访问另外32G远端内存要快得多。
    在支持NUMA架构的多核处理器下,ZGC优先在线程当前所处的处理器的本地内存上分配对象,以保证内存高效访问。
  • ZGC采用并发的标记-整理算法。没有内存碎片。

ZGC 缺点

  • 承受的对象分配速率不会太高,因为浮动垃圾。
    ZGC的停顿时间是在10ms以下,但是ZGC的执行时间还是远远大于这个时间的。
    假如ZGC全过程需要执行10分钟,在这个期间由于对象分配速率很高,将创建大量的新对象,这些对象很难进入当次GC,会被直接判定为存活对象,而本轮GC回收期间可能新分配的对象会有大部分对象都成为了“垃圾”,这些只能等到下次GC才能回收的对象就是浮动垃圾。可能造成回收到的内存空间小于期间并发产生的浮动垃圾所占的空间。
    这个问题通过分代回收能有很大优化,但是目前ZGC还不支持分代。
  • ZGC目前不支持分代回收
    ZGC目前没有实现分代回收,每次都需要进行全堆扫描,导致一些“朝生夕死”的对象没能及时的被回收。所以就不存在Young GC、Old GC,所有的GC行为都是Full GC。
  • ZGC在OpenJDK上只有在JDK17以后才正式可用
    Oracle HotSpotJDK,Adopt OpenJDK等常用JDK在低版本均无生产可用的ZGC,虽然OpenJDK中的ZGC在Java15中正式生产可用,但是Java17才是Java11之后的下一个长期稳定版。可以通过选择AliJDK,TencentJDK等试用规避此问题。

ZGC使用

低版本可用

ZGC参数说明

ZGC的垃圾回收什么情况下会被触发?

ZGC中目前会有四种机制导致GC被触发:

  • ①定时触发,默认为不使用,可通过ZCollectionInterval参数配置。
  • ②预热触发,最多三次,在堆内存达到10%、20%、30%时触发,主要时统计GC时间,为其他GC机制使用。
  • ③分配速率,基于正态分布统计,计算内存99.9%可能的最大分配速率,以及此速率下内存将要耗尽的时间点,在耗尽之前触发GC「耗尽时间 - 一次GC最大持续时间 - 一次GC检测周期时间」。
  • ④主动触发,默认开启,可通过ZProactive参数配置,距上次GC堆内存增长10%,或超过5分钟时,对比「距上次GC的间隔时间」和「49*一次GC的最大持续时间」,超过则触发。

ZGC调优

ZGC 相当智能,我们需要调整的参数很少,由于 ZGC 已经自动将垃圾回收时间控制在 10ms 左右,我们主要关心的是垃圾回收的次数和避免并发回收失败导致的长停顿。

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

ZGC有多种GC触发机制

  • 阻塞内存分配请求触发:
    当垃圾来不及回收,垃圾将堆占满时,会导致部分线程阻塞。日志中关键字是“Allocation Stall”。

  • 基于分配速率的自适应算法:
    最主要的 GC 触发方式,其算法原理可简单描述为” ZGC 根据近期的对象分配速率以及 GC 时间,计算出当内存占用达到什么阈值时触发下一次 GC ”。日志中关键字是“Allocation Rate”。

  • 基于固定时间间隔:
    通过ZCollectionInterval控制,适合应对突增流量场景。流量平稳变化时,自适应算法可能在堆使用率达到95%以上才触发GC。流量突增时,自适应算法触发的时机可能会过晚,导致部分线程阻塞。我们通过调整此参数解决流量突增场景的问题,比如定时活动、秒杀等场景。日志中关键字是“Timer”。

  • 主动触发规则:
    类似于固定间隔规则,但时间间隔不固定,是 ZGC 自行算出来的时机。日志中关键字是“Proactive”。其中,最主要使用的是 Allacation Stall GC 和 Allocation Rate GC。我们的调优思路为尽量不出现 Allocation Stall GC , 然后 Allocation Rate GC 尽量少。为了做到不出现 Allocation Stall GC ,我们需要做到垃圾尽量提前回收,不要让堆被占满,所以我们需要在堆内存占满前进行 Allocation Rate GC 。为了 Allocation Rate GC 尽量少,我们需要提高堆的利用率,尽量在堆占用 80% 以上进行 Allocation Rate GC 。基于此,Oracle 官方 ZGC 调优指南只建议我们调整两个参数:

  • 预热规则:
    服务刚启动时出现,一般不需要关注。日志中关键字是“Warmup”。

  • 外部触发:
    代码中显式调用System.gc()触发。 日志中关键字是“System.gc()”。

  • 元数据分配触发:
    元数据区不足时导致,一般不需要关注。 日志中关键字是“Metadata GC Threshold”。

参考

JVM成神路之GC分区篇:G1、ZGC、ShenandoahGC高性能收集器深入剖析

ZGC在去哪儿机票运价系统实践

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_96314.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

RHCE——shell脚本练习

一.实验要求 1、判断web服务是否运行&#xff08;1、查看进程的方式判断该程序是否运行&#xff0c;2、通过查看端口的方式判断该程序是否运行&#xff09;&#xff0c;如果没有运行&#xff0c;则启动该服务并配置防火墙规则。 ​2、使用curl命令访问第二题的web服务&#xff…

Vulnhub靶场DC-1练习

目录0x00 准备0x01 主机信息收集0x02 站点信息收集0x03 漏洞查找与利用0x00 准备 下载链接&#xff1a;https://download.vulnhub.com/dc/DC-1.zip 介绍&#xff1a;There are five flags in total, but the ultimate goal is to find and read the flag in root’s home dir…

Linux宝塔安装msyql服务,默认密码,允许远程登录问题解决

一、首先我在宝塔安装mysql服务是5.7 1.1MySQL数据库5.6之前&#xff08;不包括&#xff09;默认密码为空&#xff0c;用户不用输入密码&#xff0c;直接回车登陆 mysql -uroot -p password:(空) 1.2.MySQL数据库5.6之后&#xff08;包括&#xff09;默认密码是MySQL数据库随机…

Springboot基础学习之(十四):修改使用数据库中的数据源,修改为Druid:通过Druid实现后台监控

文章的顺序&#xff0c;是本人学习Springboot这个框架的先后顺序 这一篇文章讲解的是如何整合数据库中的数据源 Java程序很大一部分要操作数据库&#xff0c;为了提高性能操作数据库的时候&#xff0c;又不得不使用数据库连接池。 Druid 是阿里巴巴开源平台上一个数据库连接池实…

web综合

一&#xff0c;基于域名访问www.openlab.com 在文件当中写入IP与域名的映射关系 在windows中写入 也可以在客户端的/etc/hosts下写入映射关系 创建目录 [rootserver ~]# mkdir -pv /www/openlab 将所需要的内容写入对应目录当中 [rootserver ~]# echo welcome to openlab ! &…

五分钟排查Linux的健康状态

五分钟排查Linux的健康状态1. CPU1.1 top命令1.2 什么是负载1.3 vmstat2. 内存2.1 观测命令2.2 CPU缓存2.3 HugePage2.4 预先加载3. I/O3.1 观测命令3.2 零拷贝4. 网络参考&#xff1a;《Linux运维实战》、xjjdog 操作系统作为所有程序的载体&#xff0c;对应用的性能影响是非常…

华为运动健康服务Health Kit 6.10.0版本新增功能速览!

华为运动健康服务&#xff08;HUAWEI Health Kit&#xff09;6.10.0 版本新增的能力有哪些&#xff1f; 阅读本文寻找答案&#xff0c;一起加入运动健康服务生态大家庭&#xff01; 一、 支持三方应用查询用户测量的连续血糖数据 符合申请Health Kit服务中开发者申请资质要求…

初识掌控板2.0、官方拓展板和配套编程软件mpython

不是广告&#xff01;&#xff01;不是广告&#xff01;&#xff01; 一、掌控板2.0概览 掌控板又名掌上联网计算机&#xff0c;是一款为青少年学习Python编程和创意制造&#xff0c;特别是物联网应用而设计的开源硬件。内置microPython开源嵌入式Python运行环境&#xff0c;可…

查询优化器:选择最优的查询路径

当我们通过解析器理解了SQL语句要干什么之后&#xff0c;接着会找查询优化器&#xff08;Optimizer&#xff09;来选择一个最优的查询路径。 可能有同学这里就不太理解什么是最优的查询路径了&#xff0c;这个看起来确实很抽象&#xff0c;当然&#xff0c;这个查询优化器的工…

C51单片机串口通信(概念部分)

1.通信的基本概念 1.1&#xff1a;串行通信与并行通信 &#xff08;1&#xff09;.串行通信 串行通信是指用一根数据线将 一个字节的八个bit位连接&#xff0c;从低位开始依次传输。 优点&#xff1a;成本便宜&#xff0c;传输稳定 缺点&#xff1a;速度慢 并行通信是指将一…

阿里云蔡英华:云智一体,让产业全面迈向智能

4月11日&#xff0c;在2023阿里云峰会上&#xff0c;阿里云智能首席商业官蔡英华表示&#xff0c;算力的飞速发展使数字化成为确定&#xff0c;使智能化成为可能。阿里云将以云计算为基石&#xff0c;以AI为引擎&#xff0c;参与到从数字化迈向智能化的划时代变革中。 基于服务…

第三十天 Maven高级

目录 Maven高级 1. 分模块设计与开发 1.1 介绍 1.2实践 1.3 总结 2. 继承与聚合 2.1 继承 2.2 聚合 2.3 继承与聚合对比 3. 私服 3.1 场景 3.2 介绍 3.3 资源上传与下载 Maven高级 Web开发讲解完毕之后&#xff0c;我们再来学习Maven高级。其实在前面的课程当中&am…

论文笔记|CVPR2023:Semantic Prompt for Few-Shot Image Recognition

论文地址&#xff1a;https://arxiv.org/pdf/2303.14123.pdf 这是一篇2023年发表在CVPR上的论文&#xff0c;论文题目是Semantic Prompt for Few-Shot Image Recognitio&#xff0c;即用于小样本图像识别的语义提示。 1 Motivation 第一&#xff0c;最近几项研究利用 语义信…

矿泉水为什么会溴酸盐超标

矿泉水为什么会溴酸盐超标&#xff1f; 水生产企业多使用臭氧消毒&#xff0c;不过&#xff0c;水生产企业不存在水运输路途遥远的问题&#xff0c;因此可以使用臭氧消毒。同时&#xff0c;也是因为臭氧在消毒后会直接变成氧气&#xff0c;所以不会有使用氯消毒后的那种味道&a…

我在“Now In Android”中学到的 9 件事

我在“Now In Android”中学到的 9 件事 Now in Android是一款功能齐全的 Android 应用程序&#xff0c;完全使用 Kotlin 和 Jetpack Compose 构建。它遵循 Android 设计和开发最佳实践&#xff0c;旨在为开发人员提供有用的参考。 https://github.com/android/nowinandroid UI…

【软考备战·希赛网每日一练】2023年4月11日

文章目录一、今日成绩二、错题总结第一题第二题第三题第四题第五题三、知识查缺题目及解析来源&#xff1a;2023年04月11日软件设计师每日一练 一、今日成绩 二、错题总结 第一题 解析&#xff1a; 策略模式&#xff1a;定义一系列算法&#xff0c;把它们一个个封装起来&#…

c++学习之c++对c的扩展1

目录 1.面向过程与面向对象的编程 2.面向对象编程的三大特点 3.c对c的扩展&#xff1a; 1.作用域运算符&#xff1a;&#xff1a; 2.命名空间 1.c命名空间&#xff08;namespace&#xff09; 2.命名空间的使用 1.在不同命名空间内可以创建相同的名称 2.命名空间只能在全…

2.30、守护进程(1)

2.30、守护进程&#xff08;1&#xff09;1.终端是什么2.进程组是什么3.会话是什么4.进程组、会话、控制终端之间的关系5.进程组、会话操作有哪些函数①pid_t getpgrp(void);②pid_t getpgid(pid_t pid);③int setpgid(pid_t pid, pid_t pgid);④pid_t getsid(pid_t pid);⑥pid…

Java 在循环的try catch中使用continue、break

循环的try catch中使用continue、break。 结论&#xff1a;1. 循环内catch代码端中的的continue、break可以正常生效。 2. 无论是continue还是break&#xff0c;退出循环前都会执行finally中的代码 文章目录代码&#xff1a;情形1&#xff08;无continue、break&#xff09;结果…

HTTP协议状态码大全 | 汇总HTTP所有状态码

&#x1f50a; HTTP 状态码 当浏览者访问一个网页时&#xff0c;浏览者的浏览器会向网页所在服务器发出请求。当浏览器接收并显示网页前&#xff0c;此网页所在的服务器会返回一个包含 HTTP 状态码的信息头&#xff08;server header&#xff09;用以响应浏览器的请求。 HTTP…