十八.升职加薪系列-JVM垃圾回收器-开天辟地的ZGC

前言

随着Java的发展,JVM的GC垃圾回收器也在跟着升级,从早起的单线程垃圾回收器Serial,到多线程的垃圾回收器Parallel Scavenge,再到并发垃圾回收器CMS,G1等。它们在某些对延迟要求比较高的系统来说都有些力不从心,比如:12346,股票,基金等业务。JVM垃圾器的STW机制(Stop The World)让程序的性能始终没法达到最优。

一.认识ZGC

1.GC垃圾回收器的发展史

JVM(Java Virtual Machine)垃圾回收器的发展是一个持续演进的过程,旨在提供更高效、更可靠的内存管理机制。以下是对JVM垃圾回收器的发展历程、主要优点和缺点的详细介绍。
在这里插入图片描述

  • 早期垃圾回收器
    Serial GC:作为JDK 1.3.1引入的第一款GC,Serial GC是单线程的,它在进行垃圾回收时会暂停所有的应用线程,即所谓的“Stop-The-World”(STW)。

    ParNew GC:作为Serial GC的多线程版本,ParNew GC利用多个线程进行垃圾回收,但同样会暂停应用线程。

  • 并行垃圾回收器

    Parallel GC:在JDK 1.4.2中引入,Parallel GC在JDK 6之后成为HotSpot的默认GC。它支持多线程并行收集,但仍然存在STW问题。

  • 并发标记-清除垃圾回收器

    CMS(Concurrent Mark-Sweep)GC:CMS GC旨在减少STW的时间,它使用标记-清除算法,并发地进行垃圾回收。然而,由于CMS GC在并发过程中会产生内存碎片,因此在JDK 9中被标记为废弃,并在JDK 14中被移除。

  • G1(Garbage-First)垃圾回收器

    G1 GC:在JDK 1.7u4中引入,G1 GC在JDK 9中成为默认垃圾回收器。G1 GC将堆内存划分为多个固定大小的区域(Region),并使用增量和并发的方式进行垃圾回收。G1 GC通过动态调整回收策略,实现了可预测的停顿时间,并且减少了内存碎片的产生。

  • 低延迟垃圾回收器ZGC

    ZGC:在JDK 11中引入,ZGC是一个可伸缩的低延迟垃圾回收器。它使用着色指针和读屏障技术,实现了并发标记和清理,几乎不产生STW。它支持:16TB级别的堆,它可以控制在1ms内完成垃圾回收且不会随着内存增大而增加时间。

2.为什么会有ZGC(Z Garbage Collector)

近些年来,服务器的性能越来越强劲,各种应用可使用的堆内存也越来越大,常见的堆大小从 10G 到百 G 级别,部分机型甚至可以到达 TB 级别,在这类大堆应用上,传统的 GC,如 CMS、G1 的停顿时间也跟随着堆大小的增长而同步增加,即堆大小指数级增长时,停顿时间也会指数级增长。特别是当触发 Full GC 时,停顿可达分钟级别(百GB级别的堆)。当业务应用需要提供高服务级别协议(Service Level Agreement,SLA),例如 99.99% 的响应时间不能超过 100ms,此时 CMS、G1 等就无法满足业务的需求。

JDK11中推出的一款低延迟垃圾回收器ZGC,它支持16TB级别的堆,停顿时间(STW)不超过1ms,且不会随着堆的大小增加而增加。ZGC的出现是为了解决传统垃圾回收器在支持大规模内存、降低停顿时间和保持程序吞吐量方面的不足,满足现代应用对高性能、实时性的需求。

ZGC(Z Garbage Collector)是Java虚拟机(JVM)中一款专为大规模、高吞吐量应用设计的低延迟垃圾回收器。ZGC通过优化算法和硬件支持,将Stop-The-World(STW)时间控制在一毫秒以内,即使在处理TB级堆内存时也能保持稳定的性能。ZGC采用基于Region的内存布局和染色指针技术,实现了高效的内存管理和对象跟踪。通过并发执行和单代、部分回收策略,ZGC能够在不显著降低程序吞吐量的前提下,显著降低垃圾回收过程中的停顿时间。ZGC的出现是为了解决传统垃圾回收器在支持大规模内存、降低停顿时间和保持程序吞吐量方面的不足,满足现代应用对高性能、实时性的需求。

二.ZGC核心概念

1.ZGC的内存布局

在这里插入图片描述
在ZGC中,为了更有效地管理内存,它采用了分页模型,将内存划分为不同大小的页面,包括小页面、中页面和大页面。

  1. 小页面:容量固定为2MB,用于存放小于256KB的对象,由于小页面中的对象通常较小且生命周期较短,因此ZGC会优先回收小页面,以提高内存使用效率。
  2. 中页面:容量固定为32MB,用于存放大小在256KB(包括)到4MB(不包括)之间的对象,相较于小页面,中页面中存放的对象较大,但数量较少,因此,ZGC在回收时会尽量避免回收中页面,以减少不必要的性能开销。
  3. 大页面:大于32MB,容量不固定,但必须是2MB的整数倍,用于存放大于或等于4MB的对象,由于大页面中的对象通常非常大,且数量相对较少,因此ZGC在回收时会尽量避免回收大页面,以确保应用的稳定性和性能。

ZGC通过分页模型将内存划分为不同大小的页面,以更有效地管理内存和进行垃圾回收。小页面用于存放小对象,优先回收;中页面和大页面用于存放大对象,尽量避免回收。这样设计的很重要的一个原因是因为Linux Kernel 2.6引入的标准大页(huge page ,标准大页有两种常见的格式大小,分别是2MB和1GB),ZGC的内存页面大小与Linux操作系统的页面大小管理有相似之处,ZGC这么设置也是为了适应现代硬件架构的发展,提升性能。

2.ZGC指针着色技术

ZGC 出现之前, GC 信息保存在对象头的 Mark Word 中,如对象的哈希码、分代年龄、锁记录等就是这样存储的。而ZGC的着色指针将这些信息直接标记在引用对象的指针上。

着色指针(Colored Pointers)是一种内存管理优化技术,用于在64位虚拟机中更有效地利用内存地址空间。在64位系统中,指针通常占用8个字节,这足以表示接近无限的内存空间。然而,大多数应用程序使用的内存远小于这个范围,因此指针的高位通常是未使用的。着色指针技术正是利用了这些未使用的位来存储关于对象状态的信息

对于JVM来说,一个对象的地址只使用前42位,而第43-46位用来存储额外的信息,即GC对象处于ZGC那个阶段。只使用46位的客观原因是linux系统只支持46位的物理地址空间,即64T的内存,如果一定想要使用更大的内存,需要linux额外的设置。但是这个内存设置在主流的服务器上都够用了。

在引用地址的划分上,对象引用第43位表示marked0标记,44位marked1标记,46位remapped标记,指针染色就是给对应的位置为1,当然这三个位同一个时间只能有一个位生效。这些标记分别表示对象处于GC的那个阶段里
在这里插入图片描述
如图所示,指针的高18位(jdk13之后,扩展到高16位)不能用来寻址,但剩余的46位指针所支持的64TB仍能满足大多数的服务器需求。ZGC将其高4位提取出来存储4个标志信息。通过这些标志位,虚拟机可以直接从指针中看到其引用对象的三色标记状态(Marked0/Marked1)、是否进入了重分配集(即被移动过,Remapped)、是否只能通过finalize()方法才能访问到。
在这里插入图片描述

由于这些标记位进一步压缩了原本就只有46位的地址空间,也直接导致ZGC能够管理的内存不可以超过4TB(2的42次方),而通过jdk13的扩展,现在ZGC可以管理的内存空间范围为(8MB-16TB,2的44次方)。每个对象有一个64位指针,这64位被分为:

为什么有2个mark标记?

M0和M1用于在垃圾收集过程中的标记阶段标记活跃对象。这两个视图是交替使用的,以便区分不同垃圾收集周期中的对象状态。即:2个mark标记位是用来辅助GC的, 每一个GC周期开始时,会交换使用的标记位,使上次GC周期中修正的已标记状态失效,所有引用都变成未标记。

  • GC周期1:使用mark0, 则周期结束所有引用mark标记都会成为01。
  • GC周期2:使用mark1, 则期待的mark标记10,所有引用都能被重新标记。

Remapped 重定位

Remapped : Remapped视图用于表示对象已经被重定位(即移动到了新的内存地址)。在ZGC中,当一个对象被移动时,所有指向该对象的指针都需要更新以反映新的地址。Remapped视图就是用来标记这些已经更新过地址的指针。

通过使用Remapped视图,ZGC可以在不暂停应用程序的情况下完成对象的重定位。当应用程序线程访问一个Remapped视图的对象时,它会自动通过读屏障机制获取对象的最新地址,从而避免了长时间的STW暂停。

指针着色不是说真的给指针标记颜色,而是当64位的高位(M0,M1等)被标记为1,代表给指针着色了,在其他的垃圾回收器中,比如CMS,他是通过把GC的状态信息存储到对象头中来实现垃圾回收的过程,而ZGC用的是指针着色技术。相比于传统的标记对象的算法,ZGC在指针上做标记,避免了整体Stop-The-World(STW)的情况。通过着色指针,ZGC能够更精细地管理内存,并在程序运行时快速判断对象的存储状态。

其实这里是很难理解的,我们先知道ZGC使用指针着色技术来实现垃圾回收,ZGC的指针着色技术通过在指针上添加额外的信息来标记内存状态就可以了,后面我们通过具体的案例来理解。

三.ZGC垃圾回收流程

ZGC垃圾回收分为3个大的阶段,垃圾标记阶段和对象转移(对象移动或者复制)阶段和指针重定位

  • 标记(mark):从根集合出发,标记活跃对象;此时内存中存在活跃对象和已死亡对象。
  • 转移(relocate):把活跃对象转移(复制)到新的内存上,原来的内存空间可以回收。
  • 重定位(remap):因为对象的内存地址发生了变化,所以所有指向对象老地址的指针都要调整到对象新的地址上。

1.垃圾标记

ZGC使用可达性分析算法来标记垃圾,前面有讲过这日不赘述,见:《JVM垃圾回算法详解》。要注意的是:和其他垃圾回收器不同的是,ZGC是在指针上标记而不是对象上

在这里插入图片描述

为了减少STW的停顿时间,ZGC的垃圾标记分为:初始标记 和 并发标记 和 再标记三个阶段。

  • 初始标记 :和CMS,G1一样 ,ZGC会标记出所有的根对象(GC Roots),包括线程栈上的引用、静态变量和一些特殊的对象。此阶段需要暂停(STW,Stop-The-World),但耗时非常短,因为只需要扫描所有GC Roots,其处理时间和GC Roots的数量成正比。

  • 并发标记:并发地遍历堆中的对象,并标记出这些对象的存活状态。同时,ZGC会将存活对象从旧的内存区域重定位到新的内存区域,以便为后续的对象分配提供更大的连续空间。此阶段不需要暂停(没有STW),业务线程与GC线程同时运行。但需要注意的是,此阶段可能会产生漏标问题

    漏标:在并发标记的过程中如果出现对象引用关系的变化就会出现漏标问题,如下图:加入在垃圾标记的过程中C和D的引用被删除了,那么D就会被标记为垃圾,然后某对象又建立了D的引用,这种情况就是漏标。或者在这个过程中产生了一些新的对象。所以还需要一次再标记。
    在这里插入图片描述

  • 再标记:在并发标记阶段期间,应用程序可能会继续产生新的对象,而这些新对象也需要被标记为存活。因此,ZGC需要进行一次再标记阶段,以标记并更新在并发标记期间产生的新对象。此阶段需要暂停(STW),但时间很短,最多1ms。如果超过1ms,则再次进入并发标记阶段。通过SATB算法解决(G1中的解决漏标的方案)。

2.对象转移

对象转移:就是存活对象往复制到新的内存区域,ZGC在对象转移的过程中会同时用到复制算法 和 标记整理算法,前面有说过ZGC把内存分为大小不等的多个 页 ,那么如果

  • 对象在不同的页面进行转移:就等同于复制算法
  • 对象在同一个页面进行转移:就等同于整理算法

比如下图:A和B是存活对象,GC后需要转移到另外一个“页”中
在这里插入图片描述

ZGC对象的转移分为:并发转移准备,初始转移,并发转移,并发重映射几个阶段

  • 并发转移准备 :对象转移的前置工作,在该阶段会分析最有价值GC分页。该阶段不会STW
  • 初始转移 :转移初始标记的存活对象同时做对象重定位,该阶段有STW
  • 并发转移 :对转移并发标记的存活对象做转移,该阶段不会STW

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

3.图例演示

下面我用一个图来演示ZGC的回收流程,假设有:A,B,C三个对象,初始状态他们呈现出蓝色对象:一般对象创建后,或者垃圾回收完成后进行处于重定位状态所以是蓝色
在这里插入图片描述
那么当GC出发:初始标记,那么GC Root引用的对象如果地址视图是是Remapped(蓝色),会被标记为 “绿色”,就把对象地址视图切换到 Marked0,如果对象地址视图已经是 Marked0,说明已经被其他标记线程访问过了,跳过不处理,也就是这些对象的指针:43位为1,代表A对象是存活状态。此阶段会 STW
在这里插入图片描述
接着GC会沿着A对象并发标记:B , C ,都会被标记为 “绿色”,也就是他们的引用指针M0的位被标记为 1 ,代表他们都是存活对象。而在该阶段没被标记的就是"垃圾"。如果在并发标记的过程中产生了新的对象,或者对象的引用关系发生改变就会产生漏标的情况。
在这里插入图片描述

标记结束后,如果对象地址视图是 Marked0,那就是活跃的,如果对象地址视图是 Remapped,那就是不活跃的。然后就是再标记:触发STW,对并发标记中漏标的对象进行标记。

标记完之后就会触发转移阶段。首先是并发转移准备(无STW),这个阶段会分析哪些区域垃圾比较多回收价值大,以及对没有存活对象的区域执行清除工作。

紧接着就会触发:初始转移,该阶段会把 GC Roots引用的对象(绿色)转移到另外一个页中。然后就会触发 Remapped ,也就是把GC Roots的指针重新指向A的新的地址。此时对象的只指针成:蓝色,也就是Remapped 位为:1
在这里插入图片描述
接着就是触发:并发转移 ,也就是沿着A继续转移 B , C 对象,转移成功后再进行Remapped 操作。并发转移后原page页面就会被清除掉(复制算法)。这里会为每个对象在转发表中,记录从旧对象到新对象的地址转向关系。
在这里插入图片描述
ZGC收集器能仅从引用上就明确知道一个对象是否处于重分配集(从 marked0、marked1、Remapped 的标记看出),如果用户线程此时并发的访问位于重分配集中的对象,这次访问就会被预置的内存屏障拦截(只有从堆内存中读取对象的引用时,才会触发),然后立即根据转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC将这种行为称为指针的自愈(Self-Healing)

那么在下一次垃圾回收的时候时候又会走:初始标记,并发标记,再标记,转移准备,初始转移,并发转移,只不过第二次就是使用M1进行标记,也就是把蓝色“已经完成了重定位的对象”标记为“红色”。

这里有一种情况,如果:在第二次进行并发标记的时候发现了“绿色”指针,也就是还没完成重定位的对象,此时会做指正修正,也就是会把指正改成红色,完成重定位,从转发表中删除老地址映射关系。
在这里插入图片描述

4.转发表技术

在对象转移的过程中会出现一种情况,就是对象已经转移到另外一个页中,那么对象的地址已经发生变化,但是引用该对象的变量依然指向了旧的地址,这种问题ZGC通过转发表来解决。

也就是类似一个:HashMap,它会把对象新的地址和老的地址建立好映射,那么在对象使用引用对象的时候通过转发表来找到新的地址。完成重定位后就会删除转发表中的映射关系
在这里插入图片描述

5.读屏障

在ZGC过程中如果对象发生转移,但是指针还未指向新的对象地址那么这个时候,JVM如何正确的拿到转移后的对象呢,在前面我们讲过ZGC会为每个Page维护一个转发表,其中记录了对象的新 , 老 地址。那么ZGC就是通过 读屏障 + 转发表来处理这种问题。

读屏障是JVM向应用代码插入一小段代码的技术

  • 当从堆中读引用时,就需要加入一个Load Barrier(读屏障)
  • 判断当前指针是否Bad Color(不是本次GC的Mark颜色)
  • 修正指针:对象重定位+删除转发表记录

在ZGC进行垃圾回收的过程中,当读取处于重分配集的对象时(代表对象发生转移,但是指正还没修正完成),会被读屏障拦截,通过转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC将这种行为叫做指针的「自愈能力」。读屏障可以理解为类似于AOP,在读对象前去执行一段逻辑。
在这里插入图片描述
案例:

在这里插入图片描述

四.ZGC参数配置

ZGC 优势不仅在于其超低的 STW 停顿,也在于其参数的简单,绝大部分生产场景都可以自适应。当然,极端情况下,还是有可能需要对 ZGC 个别参数做个调整,大致可以分为三类:

  • 堆大小:Xmx。当分配速率过高,超过回收速率,造成堆内存不够时,会触发 Allocation Stall,这类 Stall 会减缓当前的用户线程。因此,当我们在 GC 日志中看到 Allocation Stall,通常可以认为堆空间偏小或者 concurrent gc threads 数偏小。
  • GC 触发时机:ZAllocationSpikeTolerance, ZCollectionInterval。ZAllocationSpikeTolerance 用来估算当前的堆内存分配速率,在当前剩余的堆内存下,ZAllocationSpikeTolerance 越大,估算的达到OOM 的时间越快,ZGC 就会更早地进行触发 GC。ZCollectionInterval 用来指定 GC 发生的间隔,以秒为单位触发 GC。
  • GC 线程:ParallelGCThreads, ConcGCThreads。ParallelGCThreads 是设置 STW 任务的 GC 线程数目,默认为 CPU 个数的 60%;ConcGCThreads 是并发阶段 GC 线程的数目,默认为 CPU 个数的12.5%。增加 GC 线程数目,可以加快 GC 完成任务,减少各个阶段的时间,但也会增加 CPU 的抢占开销,可根据生产情况调整。

1.参数配置

由上可以看出 ZGC 需要调整的参数十分简单,通常设置 Xmx 即可满足业务的需求,大大减轻 Java 开发者的负担 , 下面给出一份ZGC的参数配置

-Xms10G -Xmx10G 
-XX:ReservedCodeCacheSize=256m -XX:InitialCodeCacheSize=256m 
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC 
-XX:ConcGCThreads=2 -XX:ParallelGCThreads=6 
-XX:ZCollectionInterval=120 -XX:ZAllocationSpikeTolerance=5 
-XX:+UnlockDiagnosticVMOptions -XX:-ZProactive 
-Xlog:safepoint,classhisto*=trace,age*,gc*=info:file=/opt/logs/logs/gc-%t.log:time,tid,tags:filecount=5,filesize=50m 
  • -Xms -Xmx:堆的最大内存和最小内存,这里都设置为10G,程序的堆内存将保持10G不变。根据自己的内存情况设置

  • -XX:ReservedCodeCacheSize -XX:InitialCodeCacheSize:设置CodeCache的大小, JIT编译的代码都放在CodeCache中,一般服务64m或128m就已经足够。

  • -XX:+UnlockExperimentalVMOptions -XX:+UseZGC:启用ZGC的配置。

  • -XX:ConcGCThreads:并发回收垃圾的线程。默认是总核数的12.5%,8核CPU默认是1。调大后GC变快,但会占用程序运行时的CPU资源,吞吐会受到影响。

  • -XX:ParallelGCThreads:STW阶段使用线程数,默认是总核数的60%。

  • -XX:ZCollectionInterval:ZGC发生的最小时间间隔,单位秒。

  • -XX:ZAllocationSpikeTolerance:ZGC触发自适应算法的修正系数,默认2,数值越大,越早的触发ZGC。

  • -XX:+UnlockDiagnosticVMOptions -XX:-ZProactive:是否启用主动回收,默认开启,这里的配置表示关闭。

  • -Xlog:设置GC日志中的内容、格式、位置以及每个日志的大小。

2.ZGC典型应用场景

对于性能来说,不同的配置对性能的影响是不同的,如充足的内存下即大堆场景,ZGC 在各类 Benchmark 中能够超过 G1 大约 5% 到 20%,而在小堆情况下,则要低于 G1 大约 10%;不同的配置对于应用的影响不尽相同,开发者需要根据使用场景来合理判断。当前 ZGC 不支持压缩指针和分代 GC,其内存占用相对于 G1 来说要稍大,在小堆情况下较为明显,而在大堆情况下,这些多占用的内存则显得不那么突出。因此,以下两类应用强烈建议使用 ZGC 来提升业务体验:

  • 超大堆应用。超大堆(百 G 以上)下,CMS 或者 G1 如果发生 Full GC,停顿会在分钟级别,可能
    会造成业务的终端,强烈推荐使用 ZGC。
  • 当业务应用需要提供高服务级别协议(Service Level Agreement,SLA),例如 99.99% 的响应时
    间不能超过 100ms(基金股票等),此类应用无论堆大小,均推荐采用低停顿的 ZGC。
  • 33
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

墨家巨子@俏如来

你的鼓励是我最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值