ZGC原理整理

本文详细解析了ZGC的执行流程、特点(如STW时间短且堆大小无关),介绍了着色指针和读屏障技术,探讨了分代ZGC设计和调优参数。重点讲解了如何通过参数调整应对不同场景下的内存管理和性能优化。

ZGC原理整理

ZGC基础知识

ZGC执行流程图:
在这里插入图片描述

GC停顿指垃圾回收期间STW(Stop The World)停顿时间

ZGC 是一种可扩展的低延迟垃圾回收器,特性:

  • ZGC 在垃圾回收过程中,STW(停顿时间) 的时间不会超过 10ms,适合需要低延迟的应用。
  • 单代ZGC支持 几百兆16TB 的堆大小,分代ZGC支持 几百兆64TB 的堆大小。
  • STW(停顿时间)不会随着堆的大小,或者活跃对象的大小而增加。
  • ZGC降低了停顿时间,能降低接口的最大耗时,提升用户体验。但是吞吐量不佳,所以如果Java服务比较关注QPS(每秒的查询次数)那么G1是比较不错的选择。

在ZGC中,与G1垃圾回收器一样将堆内存划分成很多个区域,这些内存区域被称之为 Zpage

Zpage 分成三类大中小:

  • 小区域:2M,只能保存256KB内的对象。

  • 中区域:32M,保存256KB – 4M的对象。

  • 大区域:只保存一个大于4M的对象。

这样管控粒度比G1更细,这样更容易去控制停顿时间。

在G1垃圾回收器中,STW时间的主要来源是在转移阶段,将存活对象复制到别的区域会产生长时间的 STW,ZGC通过使用 着色指针(Colored Pointers)读屏障(Load Barrier) 技术解决该问题。

G1转移时需要停顿的主要原因

G1在转移过程中无法准确定位对象地址的问题,如下情况可以解释:

如果在并发情况下进行转移阶段,对象转移完之后,需要将A对对象的引用更改为新对象的引用。但是在更改前,用户线程执行A.c.count = 2,此时更改的是转移前对象中的属性
在这里插入图片描述
更改引用之后, A引用了转移之后的对象,此时获取A.c.count发现属性值依然是1,修改操作并未作用在转移后的对象上,这样就产生数据丢失的问题,所以G1只能通过停止用户线程去处理转移阶段。
在这里插入图片描述

ZGC通过着色指针读屏障解决问题。

着色指针(Colored Pointers)

着色指针将对象头中原来的8字节保存地址的指针拆分成了三部分:

  • 最低的44位,用于表示对象的地址,所以最多能表示16TB的内存空间。

  • 中间4位是颜色位,每一位只能存放0或者1,并且同一时间只有其中一位是1。

  • 还有16位并未使用。

颜色位分为:

  • 终结位:只能通过终结器访问。(黄色)

  • 重映射位(Remap):转移完之后,对象的引用关系已经完成变更。(绿色)

  • Marked0 和 Marked1:标记可达对象,Marked0Marked1 交替使用。(红色 和 蓝色)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

访问对象引用时,使用的是对象的地址。在64位虚拟机中,着色指针就是利用了8个字节中没有使用的高几位,存储了状态信息。

应用程序使用的对象地址,只是虚拟内存,操作系统会将虚拟内存转换成物理内存。所以只使用了44位去记录对象地址并不会对对象访问产生影响,不管颜色位变成多少,指针指向的都是同一个对象。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

第一次初始标记阶段

标志GC Roots直接引用的对象为存活对象,这些对象数量不多,所以停顿时间非常短。

初始阶段会标记GC Roots直接关联的对象,对引用这些对象的指针上的marked0位标记为1

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

并发标记阶段

GC Roots进行可达性分析,遍历所有对象,标记可以到达的每一个对象是否存活,对对象进行引用时用户线程使用读屏障,如果发现对象没有完成标记也会帮忙进行标记,对引用这些对象的指针上的marked0位标记为1

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

并发处理阶段

根据Zpage的GC处理效率,进行选择需要转移的Zpage,保证STW小于10ms,并创建转移映射表,用于记录转移前对象和转移后对象地址:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

转移开始阶段

转移GC Root直接关联的对象,其中不需要转移的对象 remapped 值设置成1,避免重复进行判断。

如下1和2不转移,将remapped置为1:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

进行转移,把对象转移到新的Zpage中,并在转移映射表中记录转移前对象和转移后对象地址:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

并发转移阶段

将剩余对象转移到新的ZPage中,转移之后将剩余对象的地址记入转移映射表:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

转移完之后,转移前的Zpage中的对象清空,但这部分空间并没被释放要到第二次垃圾回收的初始标记阶段进行回收,转移映射表需要保留下来,GC Roots直接引用对象的remapped修改为1。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

此时,如果用户线程访问4对象引用的5对象,用户线程会通过读屏障,检查转移映射表中是否存在该引用,存在则将4对5的引用进行重置,修改为对5`的引用,同时将remapped修改为1代表已经重新映射完。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

并发转移阶段结束之后,这一轮的垃圾回收就结束了,但其实并没有完成所有指针的重映射工作,这个工作会放到下一阶段,与下一阶段的标记阶段一起完成(因为都需要遍历整个对象图)。

第二次垃圾回收的初始标记阶段

第二次垃圾回收的初始标记阶段,沿着GC Root标记对象,新产生的对象也会一起进行标记。这一次会使用marked1,因为marked0是上一次垃圾回收了,这两个会相互切换使用。这样可以很容易区分出是这一次垃圾回收的标记阶段还是上一次垃圾回收的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如果Marked0为1,但是Remap为0,代表上一轮的重映射还没有完成,先完成重映射从转移表中找到老对象转移后的新对象,再进行标记。如果Remap为1,只需要进行标记。 Marked1修改为1,Marked0 重置为0:

(注意:Marked为1的对象,标记时用Marked1,然而新产生的对象使用 Marked0进行标记)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

将转移映射表删除,释放内存空间:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

读屏障(Load Barrier)

读屏障是JVM向应用代码插入一小段代码的技术。当应用线程从堆中读取对象引用时,就会执行这段代码。需要注意的是,仅“从堆中读取对象引用”才会触发这段代码。

ZGC中读屏障的代码作用:在对象标记和转移过程中,用于确定对象的引用地址是否满足条件,并作出相应动作,主要使用读后的屏障指令。

转移完之后,当用户线程获取一个对象引用时,会触发读后的屏障指令,如果对象指向的不是转移后的对象,用户线程会将引用指向转移后的对象。

f变量 一开始指向转移前的对象:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

通过读后屏障指令,判断如果是转移前的对象,就改写指针内容,指向转移后的对象:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这样对 f.count 进行赋值操作,操作的就是转移后的对象了:
在这里插入图片描述

ZGC通过 着色指针(Colored Pointers) 来判断对象是转移前还是转移后的,读屏障技术需要结合着色指针使用。

分代ZGC的设计

在JDK21之后,ZGC设计了年轻代和老年代,这样可以让大部分对象在年轻代回收,减少老年代的扫描次数,同样可以提升一定的性能。同时,年轻代和老年代的垃圾回收可以并行执行。

分代之后的着色指针将对象头中原来的8字节保存地址的指针拆分成了三部分:

  • 46位用来表示对象地址,最多可以表示64TB的地址空间。

  • 中间的12位为颜色位。

  • 最低4位和最高2位未使用

整个分代之后的读写屏障、着色指针的移位使用都变的异常复杂,仅作了解即可。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

总结 - ZGC核心技术:

1、着色指针(Colored Pointers)

着色指针将原来的8字节保存地址的指针拆分成了三部分,不仅能保存对象的地址,还可以保存当前对象所属的状态。

不支持32位系统、不支持指针压缩

2、读屏障(Load Barrier)

在获取对象引用判断对象所属状态,如果所属状态和当前GC阶段的颜色状态不一致,由用户线程完成本阶段的工作。

会损失一部分的性能,大约在5%~10%之间。

ZGC调优

ZGC重要配置参数

  • -Xms 和 -Xmx:堆的最大内存和最小内存
  • -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日志中的内容、格式、位置以及每个日志的大小。

ZGC触发时

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

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

  • 阻塞内存分配请求触发:当垃圾来不及回收,垃圾将堆占满时,会导致部分线程阻塞,这个STW时间是比较长的。日志中关键字是Allocation Stall
  • 基于分配速率的自适应算法:算法原理为 ZGC根据近期的对象分配速率以及GC时间,计算出当内存占用达到什么阈值时触发下一次GC,通过ZGC会动态改变阈值来提早或者延迟触发GC来满足需求,我们也可以同ZAllocationSpikeTolerance 参数控制阈值大小来解决了一些问题。日志中关键字是Allocation Rate
  • 基于固定时间间隔:在流量突增的时候,比如定时活动、秒杀等场景,自适应算法触发的时机可能会过晚,导致部分线程阻塞。我们通过调整 ZCollectionInterval 参数解决流量突增场景的问题。日志中关键字是Timer
  • 主动触发规则:类似于固定间隔规则,但时间间隔不固定,是ZGC自行算出来的时机,我们的服务因为已经加了基于固定时间间隔的触发机制,所以通过-ZProactive参数将该功能关闭,以免GC频繁,影响服务可用性。 日志中关键字是Proactive
  • 预热规则:服务刚启动时出现,一般不需要关注。日志中关键字是Warmup
  • 外部触发:代码中显式调用 System.gc() 触发,一般实际情况会禁止调用System.gc(),所以不需要关注。 日志中关键字是System.gc()
  • 元数据分配触发:元数据区不足时导致,一般不需要关注。 日志中关键字是Metadata GC Threshold

日志中要重点关注, Heap信息:记录了GC过程中Mark、Relocate前后的堆大小变化状况。High和Low记录了其中的最大值和最小值,我们一般关注High中Used的值,如果达到100%,在GC过程中一定存在内存分配不足的情况,需要调整GC的触发时机,更早或者更快地进行GC。

注意:该日志过滤了进入安全点的信息。正常情况,在一次GC过程中还穿插着进入安全点的操作。

ZGC停顿原因

  • 初始标记:日志中Pause Mark Start。
  • 标记结束:日志中Pause Mark End。
  • 转移开始:日志中Pause Relocate Start。
  • 内存分配阻塞:当内存不足时线程会阻塞等待GC完成,关键字是”Allocation Stall”。
  • 安全点:所有线程进入到安全点后才能进行GC,ZGC定期进入安全点判断是否需要GC。先进入安全点的线程需要等待后进入安全点的线程直到所有线程挂起。
  • dump线程、内存:比如jstack、jmap命令,这些指令的执行会产生SWT,所以这些指令一般只能在开发环境中使用,不如会影响用户的使用体验。

调优案例

  1. 秒杀活动中流量突增,出现性能毛刺

    原因:自适应算法计算的GC触发间隔较长,导致GC触发不及时,引起了内存分配阻塞,导致停顿,GC日志中有大量的“Allocation Stall”日志。

    解决方法

    • 开启”基于固定时间间隔“的GC触发机制:-XX:ZCollectionInterval
    • 增大修正系数 -XX:ZAllocationSpikeTolerance,更早触发GC。
  2. 压测时,流量逐渐增大到一定程度后,出现性能毛刺

    原因:GC触发及时,但内存标记和回收速度过慢,引起内存分配阻塞,导致停顿,平均1秒GC一次,两次GC之间几乎没有间隔。

    解决方法

    • 增大-XX:ConcGCThreads, 加快并发标记和回收速度。ConcGCThreads默认值是核数的1/8。
    • 因为该参数影响系统吞吐,如果GC间隔时间大于GC周期,不建议调整该参数。
  3. 还有一些问题是因为业务代码和JIT的编译优化导致的,根据具体情况进行优化。

JDK11开始有ZGC,这个时候是单代垃圾回收器,JDK21开始ZGC有分代垃圾回收器。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值