前言
随着JDK17的占有率不断升高和SpringBoot3最低支持JDk17,JDK17很大概率会成为大家后续升级的一个选择,而JDK17上最重要的垃圾回收器G1和ZGC,也就显得格外重要。大家提前了解或者学习一下肯定是有用的。
本篇文章也默认大家了解一些垃圾回收器的知识,也不再赘述JDK8上大家比较熟悉的垃圾回收器原理和流程。
G1垃圾回收器
垃圾优先(G1)垃圾收集器针对多处理器机器,可扩展到大量内存。它试图以高概率满足垃圾收集暂停时间目标,同时实现高吞吐量,几乎不需要配置。G1旨在使用当前目标应用程序和环境提供最佳的延迟和吞吐量平衡,其特点包括:
- 堆大小可达数十GB或更大,其中超过50%的Java堆被实时数据占用。
- 对象分配和提升的速率可能会随时间变化而显着变化。
- 堆中存在大量碎片。
- 可预测的暂停时间目标不超过几百毫秒,避免长时间的垃圾收集暂停。
这在应用程序运行时使用一个或多个垃圾收集线程时最为明显。因此,与吞吐量收集器相比,虽然G1收集器的垃圾收集暂停时间通常要短得多,但应用程序的吞吐量也稍微较低。
上面就是官网对于G1的介绍,不难看出G1有几个特点。适合大内存、几乎不需要配置、可以预测STW时间、吞吐量要略低。
内存布局
G1的内存布局和之前的有一些不太一样,但是还是保留了垃圾分代的特点。内存布局如下图:
G1将内存分为不同的region,每个region大小相同。其中主要包含了四种不同的区域。年轻代Eden(图中未标字母的红色区域)、幸存者区域Suivivor(图中标有S的区域)、老区域Old(图中未标字母的蓝色区域)、大对象区域Humongous(图中标H的大块区域)。
回收流程
G1回收流程主要是两个阶段交替。young-only阶段和Space-reclamation阶段(也叫混合GC)。具体的交替图如下:
- young-only阶段是只处理年轻代的一个阶段,包含了以下流程。
- Concurrent Start:这种类型的收集在执行普通的年轻代收集的同时,还开始了标记过程。并发标记确定了老年代区域中当前可达(活动)的所有对象,以便在接下来的空间回收阶段保留这些对象。标记过程在两个特殊的停顿中完成:Remark和Cleanup。并发启动暂停还可能确定是否要继续Remark阶段,在这种情况下,会发生一个短暂的并发标记撤销阶段,然后继续进行年轻代阶段。同时,在这种情况下,不会发生Remark和Cleanup。要注意,本阶段类似CMS垃圾回收器的并发标记,不会产生STW。
- Remark:这个阶段会STW,执行引用处理和类卸载,回收完全空的区域并清理内部数据结构。在Remark和Cleanup之间,G1会计算信息,以便稍后能够并发地回收选定的老年代区域中的空闲空间,这将在Cleanup阶段中完成。
- Cleanup:这个阶段也会STW,同时,该阶段也决定是否实际进行Space-reclamation阶段。如果后续需要进行Space-reclamation阶段,young-only阶段将以一次准备Space-reclamation阶段回收为结束。
- Space-reclamation阶段会伴随多次young-only阶段,除了年轻代区域外,还会疏散一组老年代区域中的活动对象。这些收集也称为混合收集。当G1确定疏散更多的老年代区域不会产生足够的空闲空间时,Space-reclamation阶段结束。
备注:
- young-only阶段可能不会进行垃圾回收,如果计算垃圾回收的耗时远小于设定的值,会积攒多次最后一块垃圾回收。
- young-only阶段只会回收年轻代区域和大对象区域,Space-reclamation阶段不仅包含了这两个,还有老年代区域。
- G1的区域回收是使用复制算法,将region内的数据整理后复制到新的region中,当前region直接清除。
- 如果G1尝试垃圾收集后,内存还是很紧张,以至于无法找到多余的空间进行复制迁移时,会触发Full GC,这种GC是单线程的,会很慢。
上面的流程可能一部分知识有误,因为能力有限,翻译过来也存在一些不通的情况。欢迎大家指正。
参数和最佳实践
G1的一些重要参数如下:
Option and Default Value | Description |
---|---|
-XX:MaxGCPauseMillis=200 | 最大暂停时间的目标。 |
-XX:GCPauseTimeInterval =<ergo> | 最大STW间隔的目标。默认情况下G1不设定任何目标,允许G1在极端情况下连续执行垃圾收集。 |
-XX:ParallelGCThreads =<ergo> | 垃圾收集暂停期间的最大并行工作线程数。这个数值是根据虚拟机所在计算机上可用的线程数量来确定的:如果可用于进程的CPU线程数小于或等于8,则使用该数量。否则,将大于8的线程数的五分之八加到最终的线程数中。 在每次暂停开始时,最大使用的线程数还受到其他参数限制:G1不会使用超过-XX:HeapSizePerGCThread的线程数 |
-XX:ConcGCThreads =<ergo> | 用于并发工作的最大线程数。默认情况下,该值为-XX:ParallelGCThreads除以4。 |
-XX:+G1UseAdaptiveIHOP -XX:InitiatingHeapOccupancyPercent=45 | 控制初始堆占用的。默认值表明自适应已开启,并且在最初的几个收集周期中,G1将使用老年代45%的作为标记开始阈值。 |
-XX:G1HeapRegionSize=<ergo> | 堆区域的大小。默认值基于最大堆大小,并计算出大约2048个区域,然后相除得到这个值。用户指定的大小必须是2的幂次方,有效值范围为1到512 MB。 |
-XX:G1NewSizePercent=5 -XX:G1MaxNewSizePercent=60 | 年轻代的总大小,在这两个值之间变化,以当前使用的 Java 堆的百分比表示。 |
-XX:G1HeapWastePercent=5 | 垃圾回收候选中允许的未回收空间。如果垃圾回收候选列表中的可用空间低于该值,G1将停止Space-reclamation阶段。 |
-XX:G1MixedGCCountTarget=8 | Space-reclamation阶段在一系列收集中的预计持续时间。 |
-XX:G1MixedGCLiveThresholdPercent=85 | 在Space-reclamation阶段中,如果老年代区域的存活对象占用率高于这个百分比,那么这些区域将不会被回收。 |
<ergo>
代表这个值需要根据实际情况做判断。
官方对于G1的最佳实践没有说太多,对于一般应用的建议也是使用默认值即可,不怎么需要调整,因为一些参数会在G1运行中自我调整。使用过程中只需要按照大家的要求设置期望暂停时间和最大堆大小即可。然而大家需要知道的是:G1在默认配置中的目标既不是最大吞吐量也不是最低延迟,而是在高吞吐量下提供相对较小、均匀的暂停。并且,G1回收堆空间的机制和暂停时间控制会在应用程序线程和空间回收效率方面产生一些开销,这也就是大家常说的应用内存不大的话没必要考虑G1。
如果需要应用高吞吐量,则可以通过使用或提供更大的堆来放宽暂停时间目标。如果延迟是主要要求,则修改暂停时间目标。不要设置-Xmn
, -XX:NewRatio
等参数,因为这些参数会导致期望暂停时间失效。所以从JDK8迁移到JDK17时,要丢弃所有的参数配置,并且仅设置暂停时间目标和总体堆-Xmx
大小-Xms
。如果不满足要求,再按照上面的表格进行一些微调。
当然,JVM参数不能简单的通过一两篇博客就能设置好,具体的参数配置还是要结合GC的日志和对于应用的期望。本人这一块实践也比较少,也欢迎大家交流和指正。如果对这一部分感兴趣,可以看看oracle的调优建议。G1垃圾调优
ZGC垃圾回收器
先问两个小问题:
- ZGC中的Z代表什么意思?
- ZGC怎么读?“zed gee see”还是“zee gee see”?
我们使用的最新的JDK为17版本,ZGC还是一个初步实现的版本,并没有做分代,虽然是简单实现,但是也足够优秀。目前网络上也都是基于这个版本写的文章,但是我看到2023年JVM语言峰会上,JDK21上的ZGC已经是较为完善的版本了,所以本次也就直接学习JDK21版本的ZGC,不再讲述低版本的ZGC。
新版本的ZGC更加的强大,并且主打的就是更强,更智能,基本不需要参数的调整。下面是我整理的一些特点。
- 支持最大16TB的堆。
- 低延迟一毫秒到几毫秒。
- 简单的配置(几乎不需要配置)。
同时,新版本相较于ZGC的老版本也有很大的提升,具体可以看下图:
可以说,吞吐量是之前的4倍,并且内存减少了75%。所以分代ZGC是未来的趋势。
内存布局
内存布局和之前的ZGC不太一样,不再按照大小区分region,和G1类似,直接分为两种类型,年轻代和老年代。
回收流程
新版ZGC的运行流程如下图:
- 第一阶段就是并发标记,同时,这一阶段也会对上一个周期的移动的对象的引用进行重新映射。
- 并发标记结束后会有一个STW,然后就是准备并发搬迁。这个准备阶段也会做一些类卸载的操作。
- 最后就是并发迁移。
同时,年轻代和老年代的回收也是并发的,ZGC这个版本可以说一个并发垃圾回收器。
备注:
参数配置和实践
使用分代的ZGC,需要使用下面的参数:-XX:+UseZGC -XX:+ZGenerational
,只使用ZGC,可以配置为:-XX:+UseZGC
ZGC的参数很少,几乎不需要配置。
- 不需要设置年轻代的大小,这个计算太复杂,并且要很有经验才能估算出一个大概值,ZGC自己做了。
- 也不需要设置内存的回收阈值(就算堆内存满了,也可以使用原地压缩,进行原地复制,所以不会想G1出现回收失败的情况,导致Full GC)。
- 也不需要设置年轻代多长时间转换到老年代。不同的阶段可能这个值是变化的,ZGC也帮我们做了。
- 什么时候开始老年代的收集?这个成本模型计算很复杂,ZGC也不需要我们做了。
- 回收时使用多少线程?ZGC回收时能动态设置线程数,也不需要我们设置参数了。
那我们需要设置什么?只有一个参数-Xmx
就是堆的大小,听起来挺不错的。
技术难点
技术难点因为本人能力有限,而且JDK21版本23年才出来,所以一部分的技术难点分析需要看源码才能了解透彻。这里就是简单说一下。
- 颜色指针
- 读屏障
垃圾回收器未来展望
随着JDK21引入了虚拟线程的概念,这部分的垃圾回收就变的不太好处理了。因为没法找到一个时间节点去回收所有虚拟线程的垃圾。所以有了一个新的概念,线程本地GC。
结尾
本次解释了目前比较新的垃圾回收器,也简单分析了一下技术上的细节,同时对于一些未来的技术做了一个小小的展望。当然,很多都是参考一些会议上的记录,本人能力有限,可能中间一些翻译和理解有错误,也欢迎大家指正。会议的资料地址:JVMLS2023
就这样吧,结束。