低延迟垃圾回收器ZGC

1 低延迟高可用Java服务的痛点

对于低延迟和高可用的Java服务,GC停顿一直是它们的痛点。
例如一个服务,被要求在100ms内要返回结果,服务可用性要求为99.99%。假设服务收到的请求速率稳定, GC的平均停顿时间为50ms,每分钟GC 5次, 服务的平均响应耗时为60ms,则0.4%(5*50ms/60000ms)的请求处理时间会因为GC停顿增加50ms,服务的可用性最低可能为99.6%,无法满足可用性要求。

ZGC(The Z Garbage Collector)是JDK 11中推出的一款低延迟垃圾回收器,可用于大内存低延迟服务的内存管理和回收。

2 GC基础概念

  • 垃圾:不再使用的对象。在JVM中,指的是一个没有引用指向的对象,或者仅包含循环引用的多个对象。
  • GC:Garbage Collection,垃圾回收
  • STW:Stop The World,GC的停顿时间,此时所有用户线程停止活动,等待GC线程完成垃圾回收。
  • 用户线程:用于执行业务逻辑的线程。
  • GC线程:用于垃圾收集的线程。
  • 并发:在多核条件下,用户线程和GC线程同时执行。
  • 并行:在多核条件下,多个GC线程同时执行。

3 ZGC简介

3.1 特点

  1. 单代内存管理
  2. 内存分页管理:使用小、中、大三种粒度。

3.2 优点

  1. 停顿时间不会随着堆内存的增大而增长:ZGC几乎所有暂停都只依赖于根集合大小,停顿时间不会随着堆内存的大小而增加。
  2. 单次停顿时间控制在10ms之内。在实际项目中通常停顿时间小于15ms。

3.3 缺点

  1. 吞吐量低,不适合吞吐量优先的程序。
  2. CPU占用率偏高:在实际项目中,服务使用zgc后,机器CPU负载会增加10%
  3. 对机器CPU负载敏感:在实际项目中,发现机器的CPU负载达到到60%以上,ZGC的内存回收速度会下降,导致GC周期增长。
  4. 存在浮动垃圾。
  5. 当内存回收速度低于分配速度,可用内存不足时,会退化成阻塞回收,所有用户线程暂停,直到GC完成。

4 低延迟的原因

ZGC在标记、转移和重定位阶段几乎都是并发的,这是ZGC实现停顿时间小于10ms的最关键原因。
ZGC 只有 3 个需要 STW 的阶段,其中初始标记和初始转移只需要扫描所有根集合,STW 时间根集合 的数量成正比,不会耗费太多时间。再标记过程主要处理并发标记引用地址发生变化的对象,这些对象数量比较少,耗时非常短。
在这里插入图片描述

  1. 初始标记:从根集合出发,标记根集合所有引用的对象。需要STW。
  2. 并发标记:以第1步找到的标记对象为起点,进行遍历和标记。
  3. 再标记:完成对并发标记过程中尚未标记的对象的标记。需要STW。
  4. 并发转移准备:确定需要清理的页面
  5. 初始转移:将需要清理的页面中,根集合引用对象移动到新页面。需要STW。
  6. 并发转移:将需要清理的页面中,存活对象移动到新页面。

5 关键技术

5.1 内存多重映射

内存多重映射,就是把不同的虚拟内存地址映射到同一个物理内存地址上。
ZGC 使用了内存多重映射,把同一块儿物理内存映射为 Marked0、Marked1 和 Remapped 三个虚拟内存。
Marked0、Marked1 和 Remapped 这三个虚拟内存作为 ZGC 的三个视图空间,在同一个时间点内只能有一个有效。ZGC 就是通过这三个视图空间的切换,来完成并发的垃圾回收。

5.2 着色指针

着色指针是一种将信息存储在指针中的技术。
ZGC 出现之前, GC 信息保存在对象头的 Mark Word 中。
ZGC将对象存活信息存储在42~45位中。
在这里插入图片描述
JVM 可以从指针上直接看到对象的三色标记状态(Marked0、Marked1)、是否进入了重分配集(Remapped)、是否需要通过 finalize 方法来访问到(Finalizable)。无需进行对象访问就可以获得 GC 信息,这大大提高了 GC 效率。

5.3 内存布局

为了细粒度地控制内存的分配,ZGC将内存分为小、中、大三种页面:

  1. 小页面(Small Page) : 容量固定为2MB, 用于放置小于256KB的小对象。
  2. 中页面(Medium Page) : 容量固定为32MB, 用于放置大于等于256KB但小于4MB的对象。
  3. 大页面(Large Page) : 容量不固定, 可以动态变化, 但必须为2MB的整数倍, 用于放置4MB或以上的大对象。 每个大型Region中只会存放一个大对象。

5.4 读屏障

读屏障是JVM向应用代码插入一小段代码的技术。
ZGC中读屏障的代码作用:在对象标记和转移过程中,用于确定对象的引用地址是否满足条件,并作出相应动作。
当应用线程从堆中读取对象引用时,就会执行这段代码。

Object o = obj.FieldA
<Load barrier>
Object p = o //不是从堆中读取引用
o.dosomething() //不是从堆中读取引用
int i =  obj.FieldB //不是引用类型

6 异常情况说明

6.1 GC异常情况

如果内存分配速度大于回收速度,在GC执行过程中,服务可能无可用内存,这时ZGC会进入阻塞回收模式。所有的用户线程会停止,直到GC执行完毕。
这种情况,在ZGC中称之为“Allocation Stall”

6.2 内存分配异常

"Page Cache Flush"问题影响分配速度:

  • 如果各种大小对象分配速度不稳定,比如中等大小的对象突然变多,中页面不足,那么就需要把小页面或者大页面转换成中页面,转换比较耗时,从而影响内存分配速度。
  • GC日志中的关键字为“Page Cache Flush”。

7 GC日志

7.1 GC触发时机

ZGC有多种GC触发机制,具体如下:

  1. 阻塞内存分配请求触发:当垃圾来不及回收,可用内存不足时触发。应当避免出现这种触发方式。日志中关键字是“Allocation Stall”。
  2. 基于分配速率的自适应算法:最主要的GC触发方式,其算法原理可简单描述为”ZGC根据近期的对象分配速率以及GC时间,计算出当内存占用达到什么阈值时触发下一次GC”。日志中关键字是“Allocation Rate”。
  3. 基于固定时间间隔:日志中关键字是“Timer”。
  4. 主动触发规则:类似于固定间隔规则,但时间间隔不固定,是ZGC自行算出来的时机。日志中关键字是“Proactive”。
  5. 预热规则:服务刚启动时出现,一般不需要关注。日志中关键字是“Warmup”。
  6. 外部触发:代码中显式调用System.gc()触发。 日志中关键字是“System.gc()”。
  7. 元数据分配触发:元数据区不足时导致,一般不需要关注。 日志中关键字是“Metadata GC Threshold”。

7.2 GC日志解读

GC日志中每一行都注明了GC过程中的信息,关键信息如下:
在这里插入图片描述

  • Start:开始GC,并标明的GC触发的原因。
  • Phase-Pause Mark Start:初始标记,会STW。
  • Phase-Pause Mark End:再次标记,会STW。
  • Phase-Pause Relocate Start:初始转移,会STW。

GC信息统计:定时的打印垃圾收集信息,标注了10秒内、10分钟内、10个小时内的统计信息。
如下图所示。
在这里插入图片描述

8 常用GC参数

以jdk11为例,如下是一个程序的GC参数:

java -Xms256m -Xmx256m -XX:+UnlockExperimentalVMOptions -XX:+UseZGC -XX:ConcGCThreads=2 -XX:ParallelGCThreads=3 -XX:ZCollectionInterval=60 -XX:ZAllocationSpikeTolerance=4 -XX:+ZProactive -Xlog:gc*:file=gc.log:time,tid,tags -cp work.jar Main

内存大小:

  • -Xms:堆的最大内存
  • -Xmx:堆最小内存

启用zgc:

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

GC线程数设置:

  • -XX:ConcGCThreads:并发回收垃圾的线程。示例中是2,默认是总核数的12.5%。调大后GC变快,但会占用程序运行时的CPU资源,吞吐会受到影响。
  • -XX:ParallelGCThreads:STW阶段使用线程数,示例中是3,默认是总核数的60%。

触发方式设置:

  • -XX:ZCollectionInterval:ZGC发生的最小时间间隔,单位秒。示例中是60秒。
  • -XX:ZAllocationSpikeTolerance:ZGC触发自适应算法的修正系数,默认2,数值越大,越早的触发ZGC。示例中是4。
  • ZProactive:是否启用主动回收,默认开启。 -XX:-ZProactive代表关闭

9 调优案例

9.1 服务改为ZGC后,STW时间增加

背景:

  1. 服务将GC从G1改为ZGC后,晚高峰STW停顿时间上升。

分析:

  1. 观察GC日志,主要是初始标记和初始转移耗时高导致。
  2. 该服务内有大量永久生存的对象,导致从根节点扫描和转移耗时较久。

解决方法:

  1. 使用堆外内存的方式将对象从堆中移出。

9.2 流量突增,服务出现阻塞分配的情况

背景:

  1. 服务将GC从G1改为ZGC,在应对突发流量时,停顿时间过长,GC日志出现大量“Allocation Stall”。

分析:

  1. 出现突发流量,内存分配速度高于回收速度。
  2. 在出现”Allocation Stall“之前,两次GC的间隔时间基本为0.
  3. 尝试增大GC的线程数,发现无明显效果。
  4. 将内存增大30%,“Allocation Stall”消失,两次GC间隔时间变长。

解决方法:

  1. 增大内存,增大两次GC之间的间隔,保证绝大部分对象在GC开始时已处于可回收状态。

9.3 服务内存分配速率不均,导致服务性能出现毛刺

背景:

  1. 服务将GC从G1改为ZGC后,STW耗时下降,但是观察可用性监控,出现可用性下降的毛刺。

分析:

  1. 查看GC日志,出现较多“Page Cache Flush”。
  2. 比较GC日志和服务日志,定位到问题代码。
  3. 该代码定时获取并解析来自Web API的json字符串。在解析前,会申请一个2MB的空间用于存放字符串。

解决方案:

  1. 定位到内存分配不均的代码,将原有解析代码改为流式解析。

10 总结

ZGC垃圾回收过程几乎全部是并发,实际STW停顿时间极短,不到10ms。
对于低延迟高可用的Java服务,ZGC可用满足其极低的暂停需求。

11 参考资料

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值