ZGC学习笔记:ZGC简介和JDK17对ZGC的优化

00 前情提要

    不行了,之前还没写几篇文章,本来想保持住那种非常认真非常严谨的风格,保持不住了~

    反正工作时候还不是遇到问题,先google,google不到再百度一下。解决不了就向同事卖萌啦~

ZGC学习笔记:ZGC简介和JDK17对ZGC的优化_第1张图片

这图我实在找不到更高清的了,凑合看看吧(其实可以ps,但是我懒)

    然后之前是因为有一些东西很好奇嘛,比如为什么K8s弃用docker?那是真滴好奇,好歹有一个写作目的嘛,“为什么K8S要弃用Docker”。

    在想本周写啥的时候,那要不写写ZGC?

    于是就开始搜索ZGC。

    然后发现别人都已经写好了,那我写了干嘛……

    算了随便写写吧,写到哪算哪。

    有一些东东就直接翻译了啦,好烦诶。

01 ZGC简介

    ZGC是一个可扩展的低延迟垃圾收集器,能够处理8MB到16TB大小的堆,最大暂停时间为亚毫秒。

    

ZGC学习笔记:ZGC简介和JDK17对ZGC的优化_第2张图片


OpenJDK的官网只写到这里,然后让我们自己去看Wiki(链接2)……好偷懒……

        Wiki的介绍是更详细一些。

        Z Garbage Collector,也称为 ZGC,是一种可扩展的低延迟垃圾收集器,旨在满足以下目标:

    • 亚毫秒最大暂停时间

    • 暂停时间不会随着堆、live-set 或 root-set 的大小而增加

    • 处理大小从 8MB 到 16TB 的堆

    • ZGC 最初是作为 JDK 11 中的一项实验性功能引入的,并在 JDK 15 中被宣布为 Production Ready。

ZGC的几个特征:

    • 并发

    • 基于区域

    • 基于压缩

    • NUMA 感知

    • 使用染色指针

    • 使用负载屏障(原文为load barriers)

ZGC 的核心是一个并发垃圾收集器,这意味着所有繁重的工作都在 Java 线程继续执行的同时完成。这极大地减少了垃圾收集对应用程序响应时间的影响。

ZGC 项目由 HotSpot Group 赞助。

下图是截止目前为止(2020-04-17)的ZGC的发布时间表,可以看出ZGC总JDK11开始实验性推出,JDK15开始正式发布。

ZGC学习笔记:ZGC简介和JDK17对ZGC的优化_第3张图片

        ZGC的部分参数:

ZGC学习笔记:ZGC简介和JDK17对ZGC的优化_第4张图片

ZGC部分操作:

使用下述命令选项启用ZGC

-XX:+UseZGC

  1. 启用ZGC

  2. 设置堆大小

    ZGC 最重要的调优选项是设置最大堆大小(-Xmx)。由于 ZGC 是一个并发收集器,因此必须选择最大堆大小,以便 

1) 堆可以容纳应用程序的实时集,

 2) 堆中有足够的空间来允许在 GC 时处理处理分配。

需要多少空间取决于应用程序的分配率和 live-set 大小。一般而言,给 ZGC 的内存越多越好。但与此同时,浪费内存是不可取的,所以这一切都是为了在内存使用和 GC 需要运行的频率之间找到一个平衡点。

3.设置并发GC线程数

        第二个重要的选项是设置并发 GC 线程的数量 (-XX:ConcGCThreads=)。ZGC 具有自动选择此数字的启发式方法。这种启发式通常效果很好,但根据应用程序的特性,这可能需要进行调整。这个选项本质上决定了应该给 GC 多少 CPU-time(ps:这个不知道咋翻译,就叫CPU时间?先不翻译)。给了ZGC太多运行时间,GC 将从应用程序中占用过多的 CPU-time。给它太少,应用程序分配垃圾的速度可能比 GC 收集它的速度快。

        一般来说,如果低延迟(即低应用程序响应时间)是工业环境中的最大痛点,在配置相应操作时候就不需要太吝啬。理想情况下,系统的 CPU 利用率不应超过 70%。

4.返回未使用内存给操作系统

        默认情况下,ZGC 取消提交未使用的内存,将其返回给操作系统。这对于注重内存占用的应用程序和环境很有用。可以使用 -XX:-ZUncommit 禁用此功能。此外,内存不会被取消提交,因此堆大小会缩小到最小堆大小 (-Xms) 以下。这意味着如果最小堆大小 (-Xms) 配置为等于最大堆大小 (-Xmx),则此功能将被隐式禁用。

        可以使用 -XX:ZUncommitDelay= 配置取消提交延迟(默认为 300 秒)。此延迟指定内存在有资格取消提交之前应该未使用多长时间。

    注意事项:在 Linux 上,取消提交未使用的内存需要具有 FALLOC_FL_PUNCH_HOLE 支持的 fallocate(2),此特性首先出现在内核版本 3.5(用于 tmpfs)和 4.3(用于 hugetlbfs)中。

5.启用Linux的大页(large page)操作

        将 ZGC 配置为使用大页面通常会产生更好的性能(在吞吐量、延迟和启动时间方面)并且没有真正的缺点,只是设置起来稍微复杂一些。设置过程通常需要 root 权限,这就是默认情况下不启用它的原因。

        在 Linux/x86 上,大页面(英文原文为large page和huge page)的大小为 2MB。

        假设您需要一个 16G Java 堆。这意味着您需要 16G / 2M = 8192 个大页面。

        首先为大页面池分配至少 16G(8192 页)的内存。“至少”部分很重要,因为在 JVM 中启用大页面意味着不仅 GC 会尝试将这些用于 Java 堆,而且 JVM 的其他部分也会尝试将它们用于各种 内部数据结构(代码堆、标记位图等)。因此,在本例中,我们将保留 9216 个页面 (18G) 以允许 2G 的非 Java 堆分配来使用大页面。

6.启用Linux的透明大页(transparent large page)操作

        使用显式大页面(explicit large pages,就是5小节的大页面)的替代方法是使用透明大页面( transparent huge pages)。通常不建议对延迟敏感的应用程序使用透明大页面( latency sensitive,因为它往往会导致不必要的延迟峰值。但是,可能值得尝试看看系统的工作负载是否/如何受到它的影响。

        注意事项:在 Linux 上,使用启用透明大页的 ZGC 需要kernel >= 4.7。

7.启用NUMA支持

        ZGC 支持 NUMA,这意味着它会尽量将 Java 堆分配指向 NUMA 本地内存。默认情况下启用此功能。但是,如果 JVM 检测到它只能使用单个 NUMA 节点上的内存,它将自动被禁用。通常,无需担心此设置,但如果您想显式覆盖 JVM 的决定,可以使用 -XX:+UseNUMA 或 -XX:-UseNUMA 选项来实现。

        在 NUMA 机器(例如多插槽 x86 机器)上运行时,启用 NUMA 支持通常会显著提升性能。

        注:

       关于NUMA,即Non Uniform Memory Access,非统一内存管理技术。以下直接截图于百度百科:

ZGC学习笔记:ZGC简介和JDK17对ZGC的优化_第5张图片ZGC学习笔记:ZGC简介和JDK17对ZGC的优化_第6张图片

 

8.启用GC日志

打个日志而已,就截图了。

ZGC学习笔记:ZGC简介和JDK17对ZGC的优化_第7张图片

具体操作还是参考链接2。

   02 ZGC在JDK17中的最新优化

        翻译自链接4。

       JDK 17 于2021年 9 月 14 日发布。这是一个长期支持 (LTS) 版本,这意味着它将得到多年的支持和更新。这也是第一个包含 ZGC 生产就绪版本(production ready version)的 LTS 版本。 稍微回忆一下,JDK 11(以前的 LTS 版本)中包含了 ZGC 的实验版本,而 ZGC 的第一个生产就绪版本出现在 JDK 15(非 LTS 版本)中。

        因此,可以说JDK17是第一个开始推出成熟的ZGC的长期支持的ZGC版本。

        (本来还想把JDK15,JDK16啥的ZGC的翻译一下,不过既然JDK17中ZGC这么重要,就只搬运JDK17的优化好了。)

1.动态GC线程数

        长期以来,JVM 都有一个名为

        -XX:+UseDynamicNumberOfGCThreads 

的选项。它默认启用,并告诉 GC 智能地了解它用于各种操作的 GC 线程数。使用的线程数将不断重新评估,因此会随着时间而变化。这个选项很有用有几个原因。例如,很难确定给定工作负载的最佳 GC 线程数是多少。通常发生的情况是,运维人员尝试各种设置 -XX:ParallelGCThreads 和/或 -XX:ConcGCThreads (取决于使用的 GC),看看哪个似乎给出了最好的结果。更复杂的是,最佳 GC 线程数可能会随着应用程序经历不同阶段而随时间变化,因此设置固定数量的 GC 线程本质上可能不是最佳的。

        在 JDK 17 之前,ZGC 忽略 -XX:+UseDynamicNumberOfGCThreads 并始终使用固定数量的线程。在 JVM 启动期间,ZGC 使用启发式方法来决定该固定数字 (-XX:ConcGCThreads) 应该是什么。一旦设定了这个数字,它就再也不会改变了。从 JDK 17 开始,ZGC 现在支持 -XX:+UseDynamicNumberOfGCThreads 并尝试使用尽可能少、但是足够多的线程来保持以创建的速度收集垃圾。这有助于避免使用比需要更多的 CPU 时间,从而使 Java 线程可以使用更多的 CPU 时间。

        另请注意,启用此功能后,-XX:ConcGCThreads 的含义从“使用这么多线程”变为“最多使用这么多线程”。除非有一个非常规的工作负载,否则你通常不需要摆弄 -XX:ConcGCThreads。ZGC 的启发式算法会根据运行的系统的大小为机器选择合适的最大线程数。

        (注:就是说JDK17开始,ZGC的运行时线程数是动态的,-XX:ConcGCThreads 设置的是最大可用线程,但是如果更少的线程就能满足需求,ZGC就会使用更少的线程;如果运行中需要增加线程数,ZGC也会动态增加线程数)

为了说明此功能的实际作用,官方贴出了一下运行 SPECjbb2015 时的一些图表。

ZGC学习笔记:ZGC简介和JDK17对ZGC的优化_第8张图片ZGC学习笔记:ZGC简介和JDK17对ZGC的优化_第9张图片

 

        第一张图显示了整个运行过程中使用的 GC 线程数。SPECjbb2015 有一个初始加速阶段,随后是一个较长的阶段,其中负载(注入速率)逐渐增加。我们可以看到 ZGC 使用的线程数反映了它需要做的工作量来跟上。只有在少数情况下,它需要所有(在本例中为 5 个)线程。

        JDK16和JDK17的打分比较图如下。

ZGC学习笔记:ZGC简介和JDK17对ZGC的优化_第10张图片

        如果出于某种原因希望始终使用固定数量的 GC 线程(如在 JDK 16 和更早版本中),则可以使用 -XX:-UseDynamicNumberOfGCThreads 禁用此功能(注:应该没人会用吧?)。
2.快速JVM终止

        在之前使用版本的Java程序中,如果使用的垃圾回收器是 ZGC ,终止正在运行的 Java 进程(例如,通过按 Ctrl+C 或通过让应用程序调用 System.exit()), JVM 有时可能需要一段时间(在最坏的情况下为数秒)才能真正终止。这在一些需要快速宕机的场景下很烦人,如果某个场景需要快速终止程序,JVM的慢停止会导致一定问题。。

        那么,为什么之前在使用 ZGC 时,JVM 有时会需要一些时间来终止呢?原因是 JVM 的关闭顺序需要与 GC 协调,让 GC 停止正在做的事情,进入“安全”状态。ZGC 仅在空闲时处于“安全”状态,即当前不收集垃圾。如果终止信号到达时正在进行一个非常长的 GC 周期,那么 JVM 关闭序列只需等待该 GC 周期完成,然后 ZGC 变为空闲并再次进入“安全”状态。

        这已在 JDK 17 中得到解决。ZGC 现在能够中止正在进行的 GC 循环,以按需快速达到“安全”状态。终止运行 ZGC 的 JVM 现在或多或少是即时的。

3.减少标记堆栈内存使用

        ZGC做条纹标记。这是指将堆划分为条带,并分配每个 GC 线程来标记其中一个条带(strip)中的对象。这有助于最小化 GC 线程之间的共享状态,并使标记过程对缓存更加友好,因为两个 GC 线程不会在堆的同一部分标记对象。这种方法还可以在 GC 线程之间实现自然的工作平衡,因为一个条带(strip)中的工作量往往大致相同。

        下图是ZGC的基于多线程垃圾回收器对基于条带的Java堆内存的回收机制示意图。

ZGC学习笔记:ZGC简介和JDK17对ZGC的优化_第11张图片

        在 JDK 17 之前,ZGC 的标记严格遵守条带化。如果一个 GC 线程在跟踪对象图时遇到一个对象引用,该对象引用指向不属于其分配的条带的堆的一部分,则该对象引用被放置在与该其他对象关联的线程本地标记堆栈上条纹。一旦该堆栈已满(254 个条目),它就会被移交给分配给该条带处理标记的 GC 线程。将对象引用加载到尚未标记的对象的 Java 线程会做同样的事情,只是它总是将对象引用放在关联的线程本地标记堆栈上,并且不会自己做任何实际的标记工作。

        这种方法适用于大多数工作负载,但也存在病态问题。如果您有一个具有一个或多个 N:1 关系的对象图,其中 N 是一个非常大的数字,那么您可能会为标记堆栈使用大量内存(如许多 GB)。我们一直都知道这可能是一个问题,您可以编写一个小型综合测试来引发它,但我们从未真正遇到过暴露它的真实工作负载。也就是说,直到来自腾讯的 OpenJDK 贡献者报告他们在野外遇到了这个问题(注:我去,鹅厂!)。

        JDK 17 中对此的修复涉及通过以下方式放松严格条带化:

  • 对于 GC 线程,无论对象引用指向哪个条带,首先尝试标记对象(即可能跳出 GC 线程分配的条带),如果尚未标记,则将对象引用推送到关联标记 堆。

  • 对于 Java 线程,首先检查对象是否已标记,如果尚未标记,则将对象引用推送到关联的标记堆栈。

    (注:这一块我其实看的不是很懂。要是有人有兴趣讨论的话欢迎交流)。

    这些调整有助于阻止在病态 N:1 情况下过度使用标记堆栈内存,其中 GC 线程一遍又一遍地遇到相同的对象引用,将大量重复的对象引用推入标记堆栈。重复是没有用的,因为一个对象只需要标记一次。通过在推送之前进行标记,并且只推送以前未标记的对象,复制品的生产就会停止。

    我们最初有点不愿意这样做,因为 GC 线程现在正在执行原子比较和交换操作,以标记内存中属于分配给其他 GC 线程工作的条带的对象。这打破了严格的条带化,使其对缓存不太友好。Java 线程现在也在进行原子加载以查看对象是否被标记,这是他们以前没有做过的事情。同时,GC 线程完成的其他工作(扫描/跟踪对象字段和跟踪每个堆区域的活动对象/字节数)仍然遵守严格的条带化。最后,基准测试表明我们最初的担忧是没有根据的。GC 标记时间不受影响,对 Java 线程的影响也不明显。另一方面,我们现在有一个更健壮的标记方案,不会出现过多的内存使用。

        (注:所以其实出现这个问题,是不是因为只是因为某厂的代码写的太烂了……)

支持 ARM 上的 macOS

        前段时间(注:苹果2020年的秋季发布会的消息),Apple 宣布了一项将其 Mac 计算机产品线从 x86 过渡到 ARM 的长期计划。不久之后,JEP 391: macOS/AArch64 Port 提出了 JDK 到这个新平台的移植。JVM 代码库是相当模块化的,特定于操作系统和 CPU 的代码与共享平台无关代码隔离。JDK 已经支持 macOS/x86 和 Linux/Aarch64,因此支持 macOS/Aarch64 所需的主要部分已经存在。当然,任何计划发布和支持 JDK 的 macOS/Aarch64 构建的人仍然需要做一些工作,比如投资新硬件,将这个新平台集成到 CI 管道中等。

        ZGC的故事几乎相同。macOS/x86 和 Linux/Aarch64 都已经得到支持,因此主要是启用构建和测试这种新的 OS/CPU 组合的问题。从 JDK 17 开始,ZGC 在以下平台上运行:

  • Linux/x64

  • Linux/AArch64

  • macOS/x64

  • macOS/AArch64

  • Window/x64

  • Windows/AArch64

大多数 ZGC 代码库继续独立于平台。当前的代码分布如下所示:

        

ZGC学习笔记:ZGC简介和JDK17对ZGC的优化_第12张图片

用于循环和暂停的 GarbageCollectorMXBeans

        GarbageCollectorMXBean 提供有关 GC 的信息。通过这个 bean,应用程序可以提取摘要信息(到目前为止完成的 GC 次数、累计花费的 GC 时间等)并监听 GarbageCollectionNotificationInfo 通知以获取有关单个 GC 的更细粒度的信息(GC 原因、开始时间、结束时间, ETC)。

        在 JDK 17 之前,ZGC 发布了一个名为 ZGC 的单个 bean。这个 bean 提供了有关 ZGC 周期的信息。一个循环包括从开始到结束的所有 GC 阶段。大多数阶段是并发的,但有些是 Stop-The-World 暂停。虽然有关周期的信息很有用,但您可能还想知道在执行 GC 上花费了多少时间在 Stop-The-World 暂停上。此信息不适用于单个 ZGC bean。为了解决这个问题,ZGC 现在发布了两个 bean,一个称为 ZGC Cycles,一个称为 ZGC Pauses。顾名思义,每个 bean 提供的信息分别映射到周期和暂停。

总结:

        作为第一个支持生产版本ZGC的LTS版本的JDK,JDK17中对ZGC做了下述优化:

  • 支持 JVM 选项 -XX:+UseDynamicNumberOfGCThreads。此功能默认启用,并告诉 ZGC 对其使用的 GC 线程数保持智能,这通常会导致 Java 应用程序级别的更高吞吐量和更低延迟。

  • 使用了ZGC的JVM在停止运行时, 基本上是实时的,而之前版本花费的时间更多。

  • 标记算法现在通常使用更少的内存,并且不再容易出现过多的内存使用。

  • ZGC 现在可以在 macOS/Aarch64 上运行。

  • ZGC 现在发布了两个 GarbageCollectorMXBean,以提供有关 GC 周期和 GC 暂停的信息。

03 一些很搞笑的事情

        据说有哥们在面试时候被面试官问到,ZGC的Z代表的是啥?

        面试者老哥内心OS:我去葛格不讲武德啊,之前面经没有讲到这个啊。但是呆胶布,我现场编一个。

        于是他说,“ZGC的Z是英文字母的最后一个字母,这说明Oracle公司想把ZGC作为Java的最终解决方案,是一个革命性的Java垃圾回收机制解决方案,blabla。”

        面试官微微一笑贴出官网截屏:

        

ZGC学习笔记:ZGC简介和JDK17对ZGC的优化_第13张图片

翻译:ZGC的Z毛线都不表示,莫得含义。

        面试者:不听不听王八念经。

        笑死。

03 参考链接

1.https://tech.meituan.com/2020/08/06/new-zgc-practice-in-meituan.html

2.https://wiki.openjdk.java.net/display/zgc/Main

3.https://baike.baidu.com/item/NUMA/6906025?fr=aladdin

4.https://malloc.se/blog/zgc-jdk17

你可能感兴趣的:(学习,java,开发语言)