Java垃圾收集器之ZGC剖析

ZGC诞生背景

在Java中,JVM在进行垃圾回收的时候,有一个很大的问题,就是STW(Stop The World),即JVM要进行垃圾回收时,会暂停所有的业务线程。导致所有业务系统暂停,直接降低了用户体验;所以目前市面上所有的垃圾回收器优化方向也是朝着怎么缩短STW方向,进而提升用户体验。

STW(Stop The World)

垃圾回收器的发展

为了满足不同的业务需求,Java垃圾回收器的算法也在不停的迭代,对于特定的一个应用,选择其最合适的GC算法,才能更高效率的实现业务目标。尤其是近些年来伴随着各种高并发,超高并发应用的使用,要求JVM要能适应超低停顿时间,同时也带给JVM带来了应对大堆和超大堆的挑战,基于此,2018年在发布的JDK11中,加入了ZGC(A Scalable Low-Latency Garbage Collector) 垃圾回收器

垃圾回收器

ZGC介绍

ZGC(The Z Garbage Collector) 是JDK11中推出的追求低延迟的垃圾回收器,其特点包括:

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

ZGC中的内存布局

为了细粒度的控制内存的分配,和G1一样,ZGC将内存划分成小的分区,在ZGC中成为页面(page)。所以在ZGC中是没有新生代、老年代的分代概念。

ZGC中支持3种页面,分别是小页面、中页面、大页面。其中小页面指的是2MB的页面空间,中页面指的是32MB的页面空间,大页面指的是受操作系统控制的大页面。

ZGC页面

当对象大小小于等于256KB时,对象分配在小页面

当对象大小大于256KB小于4MB之间,对象分配在中页面

当对象大小大于4MB,对象分配在大页面

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

ZGC核心概念

染色指针(Color Pointers)

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

ZGC中64位指针虚拟空间使用.

ZGC中低42位表示使用中的堆空间

ZGC借几位高位来做GC相关的事情(快速实现垃圾回收中的并发标记、转移和重定位等)

ZGC垃圾回收流程

一次ZGC垃圾回收流程包括两个阶段

  1. 标记阶段(标记垃圾)
  2. 转移阶段(对象复制或移动)

ZGC垃圾回收流程

  1. 初始阶段:

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

  2. 初始标记:

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

  3. 并发标记:

    这个阶段不需要暂停(没有STW),根据初始标记找到的根对象,使用深度优先遍历对象的成员变量进行标记,来扫描剩余的所有对象,这个处理时间比较长,所以通过并发处理,业务线程与GC线程同时运行。但是这个阶段会产生漏标问题(可用过增量更新和原始快照SATB来解决)。

  4. 再标记:

    这个阶段需要暂停(STW),主要是处理并发标记中漏标的对象,通过原始快照(SATB)算法解决(G1中的解决漏标的方案)

ZGC染色指针的并发转移算法

ZGC转移阶段分为三步

  • 并发转移准备(分析最有价值GC分页,该阶段无STW)
  • 初始转移(转移初始标记的存活的对象,同时做对象重定位,该阶段有STW)
  • 并发转移(对并发标记的存活对象做转移,该阶段无STW);并发转移阶段因为GC线程和用户线程同时执行,所以在转移过程中借助转发表来完成对象转移。

图解ZGC垃圾回收过程

  1. 初始标记

    初始标记

  2. 并发标记

    并发标记

    并发标记中,用户线程和GC线程同时进行。所以这个阶段会产生浮动垃圾,即漏标。漏标可以通过增量更新和原始快照(SATB)两种方式去解决。

  3. 再标记

    再标记

    这个阶段主要解决再并发标记过程中漏标的对象,此阶段STW

  4. 初始转移

    初始转移

    这个阶段主要转移初始标记阶段标记的对象

  5. 并发转移

    并发转移

    这个阶段转移并发标记阶段的对象,因为这个阶段用户线程和GC线程同步进行,所以会有对象正在使用,如果此时将正在使用的对象转移到新的地址,而正在使用的对象还是旧的地址,此时就会有问题,为了解决这个问题,这个阶段使用了转发表来记录转移对象的新地址和旧地址映射关系。等到下一个ZGC垃圾回收阶段,再回收转发表中的对象。

根可达分析算法

在ZGC中,判断对象是否存活,是通过根可达分析算法来实现的,这个算法的基本思路就是通过一系列的称为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

根可达性分析算法

作为GC Roots的对象主要包括以下4种:

  • 虚拟机栈(栈帧种的本地变量表):各个线程调用方法堆栈中使用到的参数、局部变量、临时变量等。
  • 方法区中类静态变量:Java类的引用类型静态变量。
  • 方法区中常量:比如:字符串常量池里的引用。
  • 本地方法栈中JNI指针:(即一般说的Native方法)

ZGC参数设置

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

  • 堆大小: -Xmx

    当分配速率过高,超过回收速率,造成堆内存不够用时,会触发Allocation Stall,这个类Stall会减缓当前的用户线程。因此,当我们在GC日志中看到Allocation Stall,通常可以认为堆空间偏小或者concurrent gc threads数偏小。

  • GC触发时机: ZAllocatioSpike Tolerance,ZCollectionInterval。

    ZAllocationSpikeTolerance 用来估算当前的堆内存分配速率,在当前剩余的堆内存下,ZAllocationSpike Tolerance越大,估算的达到OOM的时间越快,ZGC就会更早地进行触发GC。ZCollectionInterval用来指定GC发生的间隔,以秒位单位触发GC

  • GC线程: ParallerGCThreads,ConcGCThreads。

    ParallelGCThreads是设置STW任务的GC线程数,默认为CPU个数的60%;ConcGCThreads是并发阶段GC线程的数目,默认为CPU个数的12.5%。增加GC线程数目,可以加快GC完成任务,减少各个阶段的时间,但也会增加CPU的抢占开销。

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!

请我喝杯咖啡吧~

支付宝
微信