大纲

ZGC出现背景

伴随着互联网的高速发展,越来越多的系统开始追求更低的延迟和更高的可用性,而一向以稳定可靠著称的java应用服却已经务苦GC久已~

此处的GC特指GC停顿,也就是我们常说的回收期间的STW(Stop The World),当STW时,所有业务线程被挂起,直到GC停顿结束。

STW带来的问题

举几个例子:

  1. 手机系统(Android)如果发生STW,用户会很敏锐的感觉到,早期安卓不如IOS的流畅久由此而来
  2. 证券交易系统,尤其是量化交易软件,如果在关键时候发生了STW,损失也是不可估量的
  3. 再拿我前公司的大数据平台来说,下游业务要求高可用(99.99%)低延时(平均小于90ms),但因为STW,一直未能达标

垃圾回收器的发展

最新的JDK19也已经发布了,伴随着也是越来越多种类的垃圾回收器的到来,怎么挑选合适的垃圾回收器也一度成了系统优化的考虑点。
在早些年(包括现在)还是很多系统仍旧停留在JDK8,那么对于这些系统,选择垃圾回收器似乎还并没有那么难:

  1. 一般业务系统,PS组合(Parallel Scavenge+Parallel Old)似乎就已经能满足需求了
  2. 而面向B端用户,追求低延时的系统会更加倾向于ParNew+CMS的组合
  3. 采用较大的堆(8G以上),并且对象分配及晋升频繁的系统甚至可以尝试G1

而随着服务器性能越来越强,可使用的堆内存也越来越大,常见的堆大小从10G到百G,部分机型甚至可以达到TB级别,在这类大堆应用上,传统的 GC,如 CMS、G1 的停顿时间也跟随着堆大小的增长而同步增加,即堆大小指
数级增长时,停顿时间也会指数级增长。特别是当触发 Full GC 时,停顿可达分钟级别(百GB级别的堆)。当业务应用需要提供 高服务级别协议(Service Level Agreement,SLA),例如 99.99% 的响应时间不能超过 100ms,此时 CMS、G1 等就无法 满足业务的需求。
为满足当前应用对于超低停顿、并应对大堆和超大堆带来的挑战,伴随着 2018 年发布的 JDK 11,A Scalable Low-Latency Garbage Collector - ZGC 应运而生。

ZGC介绍

ZGC(The Z Garbage Collector)是JDK 11中推出的一款追求极致低延迟的垃圾收集器,它曾经设计目标包括:

  • 停顿时间不超过10ms(JDK16已经达到不超过1ms)
  • 停顿时间不会随着堆的大小,或者活跃对象的大小而增加
  • 支持8MB~4TB级别的堆,JDK15后已经可以支持16TB

这么去想,如果使用ZGC来做Java项目,像对STW敏感的证券系统,游戏的系统都可以去用Java来做(以前都是C或者C++的 市场),所以ZGC的出现就是为了抢占其他语言的市场(卷!)。

从JDK11-JDK18,ZGC一直保持着持续更新的节奏,并且于JDK15正式宣布为Production Ready,大家也可以去wiki上看看目前最近的一些更新:https://wiki.openjdk.org/display/zgc/Main

其他主流垃圾回收器实践

在介绍ZGC的目标前,先用一个例子带大家看一下我们日常使用的垃圾回收器的STW时间,首先我们准备好以下代码:

public class StopWorld {/*不停往list中填充数据*///就使用不断的填充 堆 -- 触发GCpublic static class FillListThread extends Thread{List<byte[]> list = new LinkedList<>();@Overridepublic void run() {try {while(true){if(list.size()*512/1024/1024>=990){list.clear();System.out.println("list is clear");}byte[] bl;for(int i=0;i<100;i++){bl = new byte[512];list.add(bl);}}} catch (Exception e) {}}}public static void main(String[] args) {FillListThread myThread = new FillListThread(); //造成GC,造成STWmyThread.start();}
}

本次测试使用的是JDK16,另外简单解释一下上面的示例代码,开启了一个线程不停往链表中添加元素,并达到一定大小后清空链表,往复多次即可填充满Eden区从而引发GC。

  1. 首先使用PS组合(Parallel Scavenge+Parallel Old)垃圾回收器,JVM参数如下:
-XX:+UseParallelGC -Xmx2g -XX:+PrintGCDetails

GC日志显示STW时间如下,大致需要154ms:

  1. 其次使用JDK默认的G1垃圾回收器,JVM参数如下:
-Xmx2g -XX:+PrintGCDetails

GC日志显示STW时间如下,大致需要74ms:

  1. 总结

从上面两次运行结果可知,在JDK16运行环境下,无论是ps组合还是默认的垃圾回收器,都有明显的GC停顿,这对于追求超低延时的系统来说都是不能忍受的

ZGC垃圾回收器实践

同样还是使用上面的示例代码,使用ZGC参数如下:

-XX:+UseZGC -Xmx2g -XX:+PrintGCDetails

和以上两种垃圾回收器不同,ZGC的pause时间分为了多段,但是每一段时间都非常短,一次GC总计的停顿时间甚至不足0.1ms!!!

ZGC目标

这是JDK11时提出的ZGC目标,不过直至现在,ZGC单次GC的STW时间已经不会超过1MS,而且不会随堆大小变大(最大16TB)而时间变长,可见性能已经秒杀其他垃圾回收器了,那么这么强悍的性能,ZGC是如何实现的呢,那让我们继续往下~

ZGC内存布局

为了细粒度地控制内存的分配,和G1一样,ZGC将内存划分成小的分区,在ZGC中称为页面(page)。
**ZGC中没有分代的概念(新生代、老年代) **
ZGC支持3种页面,分别为小页面、中页面和大页面。
其中小页面指的是2MB的页面空间,中页面指32MB的页面空间,大页面指受操作系统控制的大页(N * 2M)。

  1. 当对象大小小于等于256KB时,对象分配在小页面。
  2. 当对象大小在256KB和4M之间,对象分配在中页面。
  3. 当对象大于4M,对象分配在大页面。

ZGC对于不同页面回收的策略也不同。简单地说,小页面优先回收;中页面和大页面则尽量不回收。

设计目的

标准大页(huge page)是Linux Kernel 2.6引入的,目的是通过使用大页内存来取代传统的4KB内存页面,以适应越来越大的系统内存,让操作系统可以支持现代硬件架构的大页面容量功能。
Huge pages 有两种格式大小: 2MB 和 1GB , 2MB 页块大小适合用于 GB 大小的内存, 1GB 页块大小适合用于 TB 级别的内存;2MB 是默认的页大小。
所以ZGC这么设置也是为了适应现代硬件架构的发展,提升性能。

ZGC支持NUMA

在过去,对于X86架构的计算机,内存控制器还没有整合进CPU,所有对内存的访问都需要通过北桥芯片来完
成。X86系统中的所有内存都可以通过CPU进行同等访问。任何CPU访问任何内存的速度是一致的,不必考虑不
同内存地址之间的差异,这称为“统一内存访问”(Uniform Memory Access,UMA)。UMA系统的架构示
意图如图所示。

在UMA中,各处理器与内存单元通过互联总线进行连接,各个CPU之间没有主从关系。之后的X86平台经历了
一场从“拼频率”到“拼核心数”的转变,越来越多的核心被尽可能地塞进了同一块芯片上,各个核心对于内
存带宽的争抢访问成为瓶颈,所以人们希望能够把CPU和内存集成在一个单元上(称Socket),这就是非统一
内存访问(Non-Uniform Memory Access,NUMA)。很明显,在NUMA下,CPU访问本地存储器的速度比
访问非本地存储器快一些。下图所示是支持NUMA处理器架构示意图。

ZGC是支持NUMA的,在进行小页面分配时会优先从本地内存分配,当不能分配时才会从远端的内存分配。对
于中页面和大页面的分配,ZGC并没有要求从本地内存分配,而是直接交给操作系统,由操作系统找到一块能
满足ZGC页面的空间。ZGC这样设计的目的在于,对于小页面,存放的都是小对象,从本地内存分配速度很
快,且不会造成内存使用的不平衡,而中页面和大页面因为需要的空间大,如果也优先从本地内存分配,极易
造成内存使用不均衡,反而影响性能。

ZGC的核心概念

指针着色技术(Color Pointers)

颜色指针可以说是ZGC的核心概念。因为他在指针中借了几个位出来做事情,所以它必须要求在64位的机器上才可以工作。并且因为要求64位的指针,也就不能支持压缩指针。

  • ZGC中低42位表示使用中的堆空间
  • ZGC借几位高位来做GC相关的事情(快速实现垃圾回收中的并发标记、转移和重定位等)

为了能直观的解释清楚什么是指针着色,我也准备了一个C语言的例子,代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <stdint.h>
int main()
{ //创建一个共享内存的文件描述符 int fd = shm_open("/example", O_RDWR | O_CREAT | O_EXCL, 0600); if (fd == -1) return 0;//防止资源泄露,需要删除。执行之后共享对象仍然存活,但是不能通过名字访问 shm_unlink("/example"); //将共享内存对象的大小设置为4字节 size_t size = sizeof(uint32_t); ftruncate(fd, size);//3次调用mmap,把一个共享内存对象映射到3个虚拟地址上 int prot = PROT_READ | PROT_WRITE; uint32_t *remapped = mmap(NULL, size, prot, MAP_SHARED, fd, 0);uint32_t *m0 = mmap(NULL, size, prot, MAP_SHARED, fd, 0); uint32_t *m1 = mmap(NULL, size, prot, MAP_SHARED, fd, 0); //关闭文件描述符 close(fd); //测试,通过一个虚拟地址设置数据,3个虚拟地址得到相同的数据 *remapped = 0xdeafbeef; printf("48bit of remapped is: %p, value of 32bit is: 0x%x\n", remapped, *remapped); printf("48bit of m0 is: %p, value of 32bit is: 0x%x\n", m0, *m0); printf("48bit of m1 is: %p, value of 32bit is: 0x%x\n", m1, *m1); return 0;
}

在Linux上通过gcc编译后运行文件,得到的执行文件:

gcc -lrt -o mapping mapping.c

然后执行下,我们来看下执行结果:

从结果我们可以发现,3个变量对应3个不同的虚拟地址。
**实地址:(32位指针)**是:0xdeafbeef <一位16进制代表4位二进制>
**虚地址:(48位指针): **
0x7f93aef8e000<虚地址remapped>
0x7f93aef8d000<虚地址m0>
0x7f93aef8c000<虚地址m1>
但是因为它们都是通过mmap映射同一个内存共享对象,所以它们的物理地址是一样的,并且它们的值都是0xdeafbeef。

ZGC流程

一次ZGC流程

  • 标记阶段(标识垃圾)

    • 初始标记 - STW
    • 并发标记
    • 再标记 - STW
  • 转移阶段(对象复制或移动)
    • 并发转移准备
    • 初始转移 - STW
    • 并发转移

这三次STW分别对应了上文的三次PAUSE时间

ZGC中初始标记和并发标记

  • 初始标记:从根集合(GC Roots)出发,找出根集合直接引用的活跃对象(根对象)
  • 并发标记:根据初始标记找到的根对象,使用深度优先遍历对象的成员变量进行标记

ZGC基于指针着色的并发标记算法

  1. **初始阶段 **

在ZGC初始化之后,此时地址视图为Remapped,程序正常运行,在内存中分配对象,满足一定条件后垃圾回收启动。

  1. **初始标记 **

这个阶段需要暂停(STW),初始标记只需要扫描所有GC Roots,其处理时间和GC Roots的数量成正比,停顿时间不会随着堆的大小或者活跃对象的大小而增加。

  1. 并发标记

这个阶段不需要暂停(没有STW),扫描剩余的所有对象,这个处理时间比较长,所以走并发,业务线程与GC线程同时运行。但是这个阶段会产生漏标问题。

  1. **再标记 **

这个阶段需要暂停(没有STW),主要处理漏标对象,通过SATB算法解决(G1中的解决漏标的方案)。

ZGC基于指针着色的并发转移算法

ZGC的转移阶段
  • 并发转移准备(分析最有价值GC分页<无STW > )
  • 初始转移(转移初始标记的存活对象同时做对象重定位<有STW> )
  • 并发转移(对转移并发标记的存活对象做转移<无STW>)
如何做到并发转移
  • 转发表(类似于HashMap)
  • 对象转移和插转发表做原子操作

ZGC基于指针着色的重定位算法

下次GC中的并发标记(同时做上次并发标记对象的重定位)
技术上:指针着色中M0和M1区分

ZGC中的读屏障

  • 涉及对象:并发转移但还没做对象重定位的对象(着色指针使用M0和M1可以区分)
  • 触发时机:在两次GC之间业务线程访问这样的对象
  • 触发操作:对象重定位+删除转发表记录(两个一起做原子操作)

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

ZGC中GC触发机制(JDK16)

预热规则:服务刚启动时出现,一般不需要关注。日志中关键字是“Warmup”。
JVM启动预热,如果从来没有发生过GC,则在堆内存使用超过10%、20%、30%时,分别触发一次GC,以收集GC数据。

**基于分配速率的自适应算法:**最主要的GC触发方式(默认方式),其算法原理可简单描述为”ZGC根据近期的对象分配速率以及GC时间,计算出当内存占用达到什么阈值时触发下一次GC”。通过ZAllocationSpikeTolerance参数控制阈值大小,该参数默认2,数值越大,越早的触发GC。日志中关键字是“Allocation Rate”。

基于固定时间间隔:通过ZCollectionInterval控制,适合应对突增流量场景。流量平稳变化时,自适应算法可能在堆使用率达到95%以上才触发GC。流量突增时,自适应算法触发的时机可能会过晚,导致部分线程阻塞。我们通过调整此参数解决流量突增场景的问题,比如定时活动、秒杀等场景。
主动触发规则:类似于固定间隔规则,但时间间隔不固定,是ZGC自行算出来的时机,我们的服务因为已经加了基于固定时间间隔的触发机制,所以通过-ZProactive参数将该功能关闭,以免GC频繁,影响服务可用性。
阻塞内存分配请求触发:当垃圾来不及回收,垃圾将堆占满时,会导致部分线程阻塞。我们应当避免出现这种触发方式。日志中关键字是“Allocation Stall”。
外部触发:代码中显式调用System.gc()触发。 日志中关键字是“System.gc()”。
元数据分配触发:元数据区不足时导致,一般不需要关注。 日志中关键字是“Metadata GC Threshold”。

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 的抢占开销,可根据生产情况调整。

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

ZGC-一款为开拓JAVA新疆土的垃圾回收器相关推荐

  1. java 几种垃圾回收器,关于java:7种jvm垃圾回收器这次全部搞懂

    前言 之前咱们解说了jvm的组成构造与垃圾回收算法等知识点,明天咱们来讲讲jvm最重要的堆内存是如何应用垃圾回收器进行垃圾回收,并且如何应用命令去配置应用这些垃圾回收器. 堆内存详解 [腾讯云]云产品 ...

  2. 本周推荐 | JDK 11 升级实践 和 Java 新特性浅探

    推荐语:学习java和jdk的新特性并积极应用,以达到优化系统,降本提效的作用,这是我们作为java研发同学的第一节课.本文从"为什么"起手,谈到"怎么做",最 ...

  3. Java 新特性总结

    Java 新特性总结¶ 总结的这些新特性,都是自己觉得在开发中实际用得上的. 简单概括下就是: JAVA1.3:普通的原始的JAVA,基本语法相信大家都见过了 JAVA1.4:assert关键字 JA ...

  4. 垃圾回收器ZGC应用分析总结

    目录 一.基本概述 二.基本关键技术知识总结 (一)三色标记法(着色指针) (二)读屏障 (三)多图映射 (四)简单场景说明ZGC并发 三.基本回收原理介绍 四.ZGC调优案例实践 (一)调优基础知识 ...

  5. java写的教育管理的项目_干货分享|推荐12款适合做Java后台管理系统的项目

    Java是一种可以撰写跨平台应用软件的面向对象的程序设计语言.Java技术具有卓越的通用性.高效性.平台移植性和安全性,广泛应用于PC.数据中心.游戏控制台.科学超级计算机.移动电话和互联网,同时拥有 ...

  6. 手把手开始构建java新项目—医疗健康管理系统(一)

    手把手开始构建java新项目-医疗健康管理系统(一) 从基础框架开始构建一个完整的上手项目,熟悉java工作流程,了解目前所使用的框架基本使用. 项目介绍 本项目作为一款应用健康管理机构的业务系统,实 ...

  7. 3G视频与智能调度开拓公交新时代

    3G视频与智能调度开拓公交新时代 文/厦门蓝斯通信有限公司市场部王新福 目前的公交安全与运营形势 根据国务院研究中心资料显示,在未来八至十年中,由于大批人口涌入城市,预计中国的城市人口将达到总人口的6 ...

  8. 十款优质企业级Java微服务开源项目(开源框架,用于学习、毕设、公司项目、私活等,减少开发工作,让您只关注业务!)

    Java微服务开源项目 前言 一.pig 二.zheng 三.SpringBlade 四.SOP 五.matecloud 六.mall 七.jeecg-boot 八.Cloud-Platform 九. ...

  9. 使用 Jtest:一款优秀的 Java 代码优化和测试工具

     Jtest 简介 Jtest 是 Parasoft 公司推出的一款针对 Java 语言的自动化代码优化和测试工具,它通过自动化实现对 Java 应用程序的单元测试和编码规范校验,从而提高代码的可 ...

最新文章

  1. SVN 图标和工具、wc.db学习
  2. 使用docker运行微信wechat的安装脚本
  3. 小米手机证书信任设置在哪里_小米手机闹钟在哪里?闹钟怎么设置?怎么找到闹钟?...
  4. 笨方法学python3怎么样_有个很笨的女朋友,是怎么样的体验?
  5. HbuilderX、Hbuilder编辑器如何使用手机调试app
  6. TS Interface(接口)
  7. Ubuntu使用零碎记录
  8. 算法提高 7-2求arccos值
  9. excel随机数_Excel生成随机数、不重复随机数技巧,试验检测办公必备
  10. 标准的软件测试文档,软件测试上线的标准是什么?
  11. 什么是电动汽车充电桩功能介绍
  12. java调用kettle自定义kettle.properties配置文件路径
  13. 已知经纬度坐标求两点间距离,用python表示
  14. 通讯业行业观察:中兴华为思科各占千秋
  15. 书单|互联网企业面试案头书之程序员软技能篇
  16. Python计算向量夹角:向量夹角计算方法详解
  17. python实现从豌豆荚批量下载样本
  18. 微信小程序开发实验2
  19. python画名字七十周年快乐用英语怎么说_一周年快乐的英语怎么说?
  20. vue页面使用饿了么UI给tabs标题增加下拉选细化分类操作

热门文章

  1. sql groud by 语句
  2. 多功能报警杆在高速服务区的应用
  3. JAVA中word转PDF缺失表格_java – 当excel(.xlsx)使用开放式办公室转换为pdf(.pdf)时,缺少工作表和页面大小问题...
  4. 鱼眼镜头畸变校正模型
  5. Python实现 身体质量指数BMI的计算(嵩天老师)
  6. 威眼局域网监控软件3.7.2发布
  7. 关于计算机经历兼职的英文作文,求一篇兼职经历的英语作文
  8. golang interface 类型变量当作某个具体类型使用
  9. 【评测】iPS细胞相关实验服务机构-魔法师的仓库
  10. 【C语言编程】古典问题:求兔子总数