JVM 垃圾收集器之 ZGC 和 ZGC LOG 详解(下)

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: JVM 垃圾收集器之 ZGC 和 ZGC LOG 详解

多重映射寻址


不同的虚拟机内存到物理内存的转换关系可以在硬件层面,操作系统层面或者软件层面来实现。 在 Linux 平台上 ZGC 采用了多重映射(Mult-Mapping)将多个不同的虚拟内存地址映射到同一个物理内存地址上,着是一种多对一映射,一位着 ZGC 在虚拟内中看到的地址空间要比时机的堆内存容量来得更大。把染色指针中的标志位看作是地址分段符,那只要将这些不同的地址分段符都映射到同一个福利内空间,经过多重映射转换后,就可以直接使用染色指针进行寻址了,如下图所示:


网络异常,图片无法展示
|


多重映射技术确实可能带来一些诸如复制大对象时会更容易这样额外的好处,但是从源头上来说,ZGC 的多重映射只是采用染色指针的衍生品,并不是为了专门的为实现其他某种特征需求而做的。


读屏障


ZGC采用的读屏障的方式来修正指针引用,由于ZGC采用的是复制整理的方式进行GC,很有可能在对象的位置改变之后指针位置尚未更新时程序调用了该对象,那么此时在程序需要并行的获取该对象的引用时,ZGC就会对该对象的指针进行读取,判断Remapped标识,如果标识为该对象位于本次需要清理的region区中,该对象则会有内存地址变化,会在指针中将新的引用地址替换原有对象的引用地址,然后再进行返回。


Object o = obj.fieldA;    // Loading an object reference from heap
<load barrier needed here>
Object p = o;             // No barrier, not a load from heap
o.doSomething();          // No barrier, not a load from heap
int i = obj.fieldB;       // No barrier, not an object reference


如此,使用读屏障便解决了并发GC的对象读取问题,

LoadBarriers的存在,所以会导致配置ZGC的应用的吞吐量会变低。官方的测试数据是需要多出额外4%的开销:


ZGC 工作过程



ZGC 的运作过程主要可以分为以下四个阶段:


网络异常,图片无法展示
|


并发标记(Concurrent Mark):与G1、Shenandoah一样,并发标记是遍历对象图做可达性分析的 阶段,前后也要经过类似于G1、Shenandoah的初始标记、最终标记(尽管ZGC中的名字不叫这些)的短暂停顿,而且这些停顿阶段所做的事情在目标上也是相类似的。与G1、Shenandoah不同的是,ZGC的标记是在指针上而不是在对象上进行的,标记阶段会更新染色指针中的Marked 0、Marked 1标志位。


并发预备重分配(Concurrent Prepare for Relocate):这个阶段需要根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集(Relocation Set)。重分配集与G1收集器的回收集(Collection Set)还是有区别的,ZGC划分Region的目的并非为了像G1那样做收益优先的增量回收。相反,ZGC每次回收都会扫描所有的Region,用范围更大的扫描成本换取省去G1中记忆集的维护成本。因此,ZGC的重分配集只是决定了里面的存活对象会被重新复制到其他的Region中,里面 的Region会被释放,而并不能说回收行为就只是针对这个集合里面的Region进行,因为标记过程是针对全堆的。此外,在JDK 12的ZGC中开始支持的类卸载以及弱引用的处理,也是在这个阶段中完成的。


并发重分配(Concurrent Relocate):重分配是ZGC执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系。得益于染色指针的支持,ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集之中,如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障所截获,然后立即根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC将这种行为称为指针的“自愈”(Self-Healing)能力。


这样做的好处是只有第一次访问旧对象会陷入转发,也就是只慢一次,对比 Shenandoah 的 Brooks 转发指针,那是每次对象访问都必须付出的固定开销,简单地说就是每 次都慢,因此 ZGC 对用户程序的运行时负载要 Shenandoah 来得更低一些。还有另外一个直接的好处是由于染色指针的存在,一旦重分配集中某个 Region 的存活对象都复制完毕后,这个 Region 就可以立即释放用于新对象的分配(但是转发表还得留着不能释放掉),哪怕堆中还有很多指向这个对象的未更新指针也没有关系,这些旧指针一旦被使用,它们都是可以自愈的。


并发重映射(Concurrent Remap):重映射所做的就是修正整个堆中指向重分配集中旧对象的所有引用,这一点从目标角度看是与 Shenandoah 并发引用更新阶段一样的,但是 ZGC 的并发重映射并不是一个必须要“迫切”去完成的任务,因为前面说过,即使是旧引用,它也是可以自愈的,最多只是第一次使用时多一次转发和修正操作。重映射清理这些旧引用的主要目的是为了不变慢(还有清理结束后可以释放转发表这样的附带收益),所以说这并不是很“迫切”。因此,ZGC 很巧妙地把并发重映射阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去完成,反正它们都是要遍历所有对象的,这样合并就节省了一次遍历对象的开销。一旦所有指针都被修正之后,原来记录新旧对象关系的转发表就可以释放掉了。


ZGC 核心参数


参数 说明
-XX:+UseZGC 启用 ZGC
-Xmx 设置最大堆内存
-Xlog:gc 打印 GC日志
-Xlog:gc* 打印 GC 详细日志


ZGC 触发时机



ZGC 中的几种种触发 GC场景:


  • **基于固定时间间隔:**默认为不使用,可以通过 ZCollectionInterval 参数配置。GC 日志中的关键字 “Timer”。


  • **启动预热触发:**最多三次,在堆内存空间达到 10%、20%、30% 时机触发、主要是通过 GC 的时间、为其他的 GC 触发准备。GC日志关键字 “Warmup”。


  • **基于分配速率的自适应算法:**基于正态分布统计,计算内存 99% 可能的最大分配速率,以及此速率下内存将要耗尽的时间点,在耗尽之前触发 GC (耗尽时间,一次 GC 最大持续时间-一次 GC 检测周期时间)。GC日志关键字 “Allocation Rate”。


  • 主动触发:(默认开启,可以通过 ZProactictive 参数配置)距上一次 GC 堆内存增长 10%,超过 5 分钟时,对比上次 GC的间隔时间限(一次 GC 最大持续时间),超过则触发。GC 日志关键字 “Proactive”。


  • **元数据分配触发:**元数据区不足导致,GC 日志关键中是 “Metadata GC Threshold”


  • 直接触发:代码中显示调用 System.gc() 触发,GC 日志关键字是 “System.gc()”。


  • 阻塞内存分配请求触发:垃圾对象来不及挥手,占满整个堆空间,导致部分线程阻塞,GC 日志关键字是 “Allocation Stall”。


ZGC 日志分析


我们将对下面的一个简单的程序做一个 ZGC LOG 做一个分析,下面是具体的代码和分析。


示例代码


下面是一段简单的代码:


/**
 * VM Args:-XX:+UseZGC -Xmx8m -Xlog:gc*
 */
public class HeapOOM {
    public static void main(String[] args) {
        List<byte[]> list = new ArrayList<>();
        while (true) {
            list.add(new byte[2048]);
        }
    }
}


GC 日志分析


GC 日志如下(运行环境 JDK 17),举个例子:


网络异常,图片无法展示
|


GC 日志中每一行都标注了对 GC 过程中的信息,关键信息如下:


  • Start:开始GC,并标明的GC触发的原因。上图中触发原因是自适应算法。


  • Phase-Pause Mark Start:初始标记,会STW。


  • Phase-Pause Mark End:再次标记,会STW。


  • Phase-Pause Relocate Start:初始转移,会STW。


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


  • GC信息统计:可以定时的打印垃圾收集信息,观察10秒内、10分钟内、10个小时内,从启动到现在的所有统计信息。利用这些统计信息,可以排查定位一些异常点。


ZGC 总结


  1. 本文主要是从概念上描述了 ZGC 的特征和工作过程。


  1. 目前大多数互联网公司还是使用  jdk 8、jdk 11 主流使用的还是 ParNew + CMS 组合或者 G1


  1. 对于我们一线 Java 开发者应该具备新技术的学习热情和关注度,才能在激烈的社会竞争中保持优势。


参考资料


  • 深入理解 JVM 虚拟机第三版 周志明





相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
3月前
|
存储 算法 Oracle
极致八股文之JVM垃圾回收器G1&ZGC详解
本文作者分享了一些垃圾回收器的执行过程,希望给大家参考。
|
16天前
|
存储 监控 算法
JVM调优深度剖析:内存模型、垃圾收集、工具与实战
【10月更文挑战第9天】在Java开发领域,Java虚拟机(JVM)的性能调优是构建高性能、高并发系统不可或缺的一部分。作为一名资深架构师,深入理解JVM的内存模型、垃圾收集机制、调优工具及其实现原理,对于提升系统的整体性能和稳定性至关重要。本文将深入探讨这些内容,并提供针对单机几十万并发系统的JVM调优策略和Java代码示例。
42 2
|
3月前
|
存储 算法 Java
JVM自动内存管理之垃圾收集算法
文章概述了JVM内存管理和垃圾收集的基本概念,提供一个关于JVM内存管理和垃圾收集的基础理解框架。
JVM自动内存管理之垃圾收集算法
|
3月前
|
存储 算法 Java
JVM组成结构详解:类加载、运行时数据区、执行引擎与垃圾收集器的协同工作
【8月更文挑战第25天】Java虚拟机(JVM)是Java平台的核心,它使Java程序能在任何支持JVM的平台上运行。JVM包含复杂的结构,如类加载子系统、运行时数据区、执行引擎、本地库接口和垃圾收集器。例如,当运行含有第三方库的程序时,类加载子系统会加载必要的.class文件;运行时数据区管理程序数据,如对象实例存储在堆中;执行引擎执行字节码;本地库接口允许Java调用本地应用程序;垃圾收集器则负责清理不再使用的对象,防止内存泄漏。这些组件协同工作,确保了Java程序的高效运行。
22 3
|
4月前
|
存储 算法 安全
(八)JVM成神路之GC分区篇:G1、ZGC、ShenandoahGC高性能收集器深入剖析
在《GC分代篇》中,我们曾对JVM中的分代GC收集器进行了全面阐述,而在本章中重点则是对JDK后续新版本中研发推出的高性能收集器进行深入剖析。
145 12
|
3月前
|
C# UED 开发者
WPF打印功能实现秘籍:从页面到纸张,带你玩转WPF打印技术大揭秘!
【8月更文挑战第31天】在WPF应用开发中,打印功能至关重要,不仅能提升用户体验,还增强了应用的实用性。本文介绍WPF打印的基础概念与实现方法,涵盖页面元素打印、打印机设置及打印预览。通过具体案例,展示了如何利用`PrintDialog`和`PrintDocument`控件添加打印支持,并使用`PrinterSettings`类进行配置,最后通过`PrintPreviewWindow`实现打印预览功能。
228 0
|
3月前
|
C# UED 开发者
WPF动画大揭秘:掌握动画技巧,让你的界面动起来,告别枯燥与乏味!
【8月更文挑战第31天】在WPF应用开发中,动画能显著提升用户体验,使其更加生动有趣。本文将介绍WPF动画的基础知识和实现方法,包括平移、缩放、旋转等常见类型,并通过示例代码展示如何使用`DoubleAnimation`创建平移动画。此外,还将介绍动画触发器的使用,帮助开发者更好地控制动画效果,提升应用的吸引力。
127 0
|
3月前
|
算法 Java 程序员
【JVM的秘密花园】揭秘垃圾收集器的神秘面纱!
【8月更文挑战第25天】在Java虚拟机(JVM)中,垃圾收集(GC)自动管理内存,回收未使用的对象以避免内存泄漏和性能下降。本文深入介绍了JVM中的GC算法,包括串行、并行、CMS及G1等类型及其工作原理。选择合适的GC策略至关重要:小型应用适合串行收集器;大型应用或多核CPU环境推荐并行收集器或CMS;需减少停顿时间时,CMS是好选择;G1适用于大堆且对停顿时间敏感的应用。理解这些能帮助开发者优化程序性能和稳定性。
35 0
|
3月前
|
算法 Java
JVM自动内存管理之垃圾收集器
这篇文章是关于Java虚拟机(JVM)自动内存管理中的垃圾收集器的详细介绍。
|
4月前
|
监控 算法 Java
深入理解Java虚拟机:垃圾收集机制的演变与最佳实践
【7月更文挑战第14天】本文将带领读者穿梭于JVM的心脏——垃圾收集器,探索其设计哲学、实现原理和性能调优。我们将从早期简单的收集算法出发,逐步深入到现代高效的垃圾收集策略,并分享一些实用的调优技巧,帮助开发者在编写和维护Java应用时做出明智的决策。
46 3