【JVM】G1垃圾收集器知多少

前言

在JDK 9中G1取代Parallel Scavenge加Parallel Old组合,成为服务端模式下的默认垃圾收集器,而CMS则沦落至被声明为不推荐使用(Deprecate)的收集器。本文主要学习G1垃圾收集器。

本文主要以JavaSE18为基础。

简介

Garbage First(简称G1)收集器是垃圾收集器技术发展历史上的里程碑式的成果,它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。

G1 旨在当前目标应用程序和环境提供延迟和吞吐量之间的最佳平衡使用,其特点包括:

并行与并发

  • G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短STW停顿时间。部分其他收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。

分代收集

  • 虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。

空间整合

  • 与CMS的标记-清理算法不同,G1从整体来看是基于标记-整理算法实现的收集器,从局部上来看是基于标记-复制算法实现的。

可预测的停顿

  • 这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型【后台维护的优先列表】,能让使用者明确指定在一个长度为M毫秒的时间片段(通过参数-XX:MaxGCPauseMillis指定)内完成垃圾收集。

显示启用

-XX:+UseG1GC

基本概念

G1收集器出现之前的所有 其他收集器,包括CMS在内,垃圾收集的目标范围要么是整个新生代(Minor GC),要么就是整个老 年代(Major GC),再要么就是整个Java堆(Full GC)。而G1可以面向堆内存任何部分来组成回收集(Collection Set,一般简称CSet)进行回收,衡量标准不再是它属于哪个分代,而 是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。

堆布局

G1 将堆划分为一组大小相等的堆区域(Region),每个区域都是连续的虚拟内存范围区域(Region)是内存分配和内存回收的单位。在任何给定时间,这些区域中的每一个都可以是空的(浅灰色),或者分配给特定的一代,无论是年轻的还是年老的。当内存请求进入时,内存管理器会分发空闲区域。内存管理器将它们分配给一代,然后将它们作为空闲空间返回给应用程序,它可以自行分配到其中。

在这里插入图片描述

年轻代包含伊甸园Eden区域(红色)和幸存者Survivor区域(红色带有“S”)。这些区域提供与其他收集器中的相应连续空间相同的功能,不同之处在于,在 G1 中,这些区域通常以非连续模式布局在内存中。老区(浅蓝色)构成老一代。对于跨越多个区域的对象,老年代区域可能是巨大的(带有“H”的浅蓝色)。

G1认为只要大小超过了一个 Region容量一半的对象即可判定为大对象(Humongous区域)。每个Region的大小可以通过参数-XX:G1HeapRegionSize设 定,取值范围为1MB~32MB,且应为2的N次幂。而对于那些超过了整个Region容量的超级大对象, 将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代 的一部分来进行看待。

应用程序总是分配到年轻代,即伊甸园区域,但直接分配为属于老年代的巨大对象除外。

虽然G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,它们都是一系列区 域(不需要连续)的动态集合。G1收集器之所以能建立可预测的停顿时间模型,是因为它将Region作 为单次回收的最小单元,即每次收集到的内存空间都是Region大小的整数倍,这样可以有计划地避免 在整个Java堆中进行全区域的垃圾收集。更具体的处理思路是让G1收集器去跟踪各个Region里面的垃 圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一 个优先级列表,每次根据用户设定允许的收集停顿时间(使用参数-XX:MaxGCPauseMillis指定,默 认值是200毫秒),优先处理回收价值收益最大的那些Region,这也就是“Garbage First”名字的由来。 这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在有限的时间内获 取尽可能高的收集效率。

垃圾回收周期

G1 收集器在两个阶段之间交替。

Young-only 阶段(Young GC) 包含垃圾收集,这些垃圾收集会逐渐用老年代中的对象填满当前可用的内存。

空间回收阶段(Mixed GC) 是G1 除了处理年轻代外,还逐步回收老年代中的空间。然后循环以仅年轻阶段重新开始。

在这里插入图片描述
该图显示了 G1 阶段的顺序以及在这些阶段中可能发生的暂停。有实心圆圈,每个圆圈代表一个垃圾回收暂停:蓝色圆圈代表young-only收集暂停,橙色代表由标记引起的暂停,红色圆圈代表混合收集暂停。暂停在两个箭头上形成一个圆圈:一个代表在young-only阶段发生的暂停,另一个代表混合收集阶段。Young-only 阶段从一些由蓝色小圆圈表示的 Young-only 垃圾收集开始。在老年代的对象占有率达到定义的阈值后 InitiatingHeapOccupancyPercent,下一个垃圾收集暂停将是初始标记垃圾收集暂停,显示为较大的蓝色圆圈。除了与其他仅限年轻的暂停相同的工作外,它还准备并发标记。

在并发标记运行时,可能会发生其他仅年轻的暂停,直到 G1 完成标记的 Remark 暂停(第一个大橙色圆圈)。在清理暂停之前,可能会发生其他仅限年轻的垃圾收集。在 Cleanup 暂停之后,将有一个最终的仅限年轻的垃圾收集,完成年轻的阶段。在空间回收阶段,将发生一系列混合收集,用红色实心圆圈表示。由于 G1 努力使空间回收尽可能高效,因此在 Young-only 阶段,通常混合垃圾收集暂停比 Young-only 暂停更少

以下列表详细描述了 G1 垃圾收集周期的阶段、它们的暂停和阶段之间的转换:

1、Young-only phase:这个阶段从一些将对象提升到老年代的正常年轻集合开始。当老年代占用率达到某个阈值,即 Initiating Heap Occupancy 阈值时,young-only 阶段和空间回收阶段之间的转换开始。此时,G1 会安排一个并发年轻代收集。

  • Concurrent Start:这种类型的收集除了执行正常的年轻收集之外,还会开始标记过程。并发标记确定老年代区域中所有当前可到达(活动)的对象,以保留用于下一个空间回收阶段。在集合标记尚未完全完成时,可能会发生正常的年轻集合。标记结束时有两个特殊的停顿:RemarkCleanup
    Concurrent Start pause 也可以确定不需要继续进行标记:在这种情况下,会发生短暂的并发标记撤消阶段,并且 Young Only 阶段会继续。在这种情况下,不会发生备注和清理暂停。

  • Remark:此暂停完成标记本身,执行引用处理和类卸载,回收完全空的区域并清理内部数据结构。在 RemarkCleanup G1 之间计算信息,以便稍后能够同时回收选定的老年代区域中的空闲空间,这将在 Cleanup 暂停中完成。

  • Cleanup:这个暂停决定了空间回收阶段是否真的会紧随其后。如果随后是空间回收阶段,则 Young-only 阶段以单个 Prepare Mixed 年轻集合完成。

2、Space-reclamation phase(空间回收阶段):这个阶段由多个年轻集合组成,除了年轻代区域外,还撤离老年代区域集的活动对象。这些集合也称为混合集合。当 G1 确定撤出更多的老年代区域不会产生足够的可用空间值得努力时,空间回收阶段就结束了。

在空间回收之后,收集周期以另一个年轻阶段重新开始。为了备份,如果应用程序在收集活动信息时内存不足,G1 会像其他收集器一样执行就地 stop-the-world 全堆压缩(Full GC)

G1垃圾收集过程

G1收集器又可以大致可划分为以下四个步骤:

·初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际 并没有额外的停顿。

·并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。

·最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留 下来的最后那少量的SATB记录。

·筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行 完成的。

在这里插入图片描述

垃圾收集暂停和收集集

G1stop-the-world 暂停中执行垃圾收集和空间回收。活动对象通常从源区域复制到堆中的一个或多个目标区域,并且对这些移动对象的现有引用进行调整。

对于非大对象区域,对象的目标区域由该对象的源区域确定:

  • 年轻代的对象(伊甸园和幸存者区域)被复制到幸存者或旧区域,具体取决于它们的年龄。

  • 旧区域的对象被复制到其他旧区域。

大对象区域中的对象被区别对待。G1 只确定它们的活跃度,如果它们不活跃,则回收它们占据的空间。大对象区域内的对象永远不会被 G1 移动。

记忆集(Remembered Set)

RSet=remeberedset。每一个region中都有一个hashtable,记录了其他region中的对象到本对象的引用。使得垃圾回收不需要扫描整个堆,就可以知道谁使用了当前region中的对象,只需要扫描Rset

为了疏散收集集G1 管理一个记忆集收集集之外的位置集,其中包含对收集集的引用。当收集集中的对象在垃圾收集期间移动时,需要更改从收集集外部对该对象的任何其他引用以指向该对象的新位置。

记忆集条目代表了节省内存的大致位置:通常引用靠得很近,引用对象靠得很近。G1 将堆逻辑划分为卡片,默认为 512 字节大小的区域。记忆集条目是这些卡片的压缩索引。

G1 最初在每个区域的基础上管理这个记忆集:每个区域都包含一个每个区域的记忆集,即具有对该区域的潜在引用的位置集。在垃圾收集期间,整个收集集记忆集是从这些生成的。

记忆集大多是懒惰地创建的:在 RemarkCleanup 暂停之间,G1 重建所有集合集候选区域的记忆集。除此之外,G1 始终维护年轻代区域的记忆集,因为它们在每次收集时都会被收集,并且默认情况下会急切回收一些巨大的对象。

收集集(Collection Set)

CSet=collection set。一组可被回收的分区的集合。把需要回收的region都集合起来,叫做collection set

收集集是要从中回收空间的源区域的集合。根据垃圾收集的类型,收集集由不同类型的区域组成:

在 Young-Only 阶段,收集集仅包含年轻代中的区域,以及具有可能被回收的对象的巨大区域。

在空间回收阶段,收集集由年轻代中的区域、具有可能被回收的对象的巨大区域以及收集集候选区域集中的一些老年代区域组成。

收集集候选区域是在空间回收阶段极有可能被收集的区域G1Remark 暂停期间根据它们包含多少实时数据以及它们与其他区域的连接性来选择它们。实时数据少的区域(大量可用空间)优于大部分活跃的区域,连通性少的区域优于连通性高的区域,因为收集这些更“有效”区域的工作量较小G1 将删除对从集合集候选区域释放内存增益贡献不大的区域。这包括可回收空间量小-XX:G1HeapWastePercent当前堆大小的百分比。G1 将不会在此空间回收阶段后期收集这些区域。

RemarkCleanup 暂停之间,G1 继续为以后的收集做准备,而 Cleanup 暂停完成工作并根据效率对它们进行排序。收集时间更短且包含更多空闲空间的更有效区域最好在后续的混合收集中收集。

G1内部

Java 堆大小

G1 在调整 Java 堆大小时遵守标准规则,使用 -XX:InitialHeapSize最小 Java 堆大小、 -XX:MaxHeapSize最大 Java 堆大小、 -XX:MinHeapFreeRatio 最小可用内存百分比 -XX:MaxHeapFreeRatio来确定调整大小后可用内存的最大百分比。G1 收集器仅在 RemarkFull GC 暂停期间考虑根据这些选项调整 Java 堆的大小。此过程可能会向操作系统释放内存或从操作系统分配内存

堆扩展发生在收集暂停内,而内存释放发生在与应用程序并发的暂停后。

Young-Only Phase Generation Sizing

G1 总是在正常的年轻收集结束时为下一个 mutator 阶段调整年轻代的大小。这样,G1 可以满足使用 -XX:MaxGCPauseTimeMillis-XX:GCPauseIntervalMillis基于对实际暂停时间的长期观察而设定的暂停时间目标。这个计算考虑了相似规模的年轻一代撤离需要多长时间。这包括诸如在收集过程中必须复制多少对象以及这些对象的相互连接程度等信息。

选项定义了最小修改-XX:GCPauseIntervalMillis-XX:MaxGCPauseTimeMillis利用率 (MMU)。-XX:GCPauseIntervalMillisG1 将尝试在最多使用 毫秒的每个可能时间范围内 -XX:MaxGCPauseTimeMillis进行垃圾收集暂停。

如果没有其他约束,则 G1 会在确定满足暂停时间的值-XX:G1NewSizePercent之间自适应调整年轻代的大小。
或者,-XX:NewSize结合使用-XX:MaxNewSize可以分别设置最小和最大年轻代大小。

笔记:XX:NewSize仅指定后一个选项之一将年轻代大小固定为分别通过 -和传递的值-XX:MaxNewSize。这将禁用暂停时间控制。

Space-Reclamation Phase Generation Sizing

在空间回收阶段,G1 尝试在一次垃圾收集暂停中最大化老年代回收的空间量。年轻代的大小通常设置为允许的最小值,通常由 确定 -XX:G1NewSizePercent,但也考虑到 MMU 规范。

在此阶段的每个混合收集开始时,G1 从集合收集候选中选择一组区域添加到集合收集中。这组额外的老年代区域由三部分组成:

确保疏散进度的最小年老代区域集。这组老年代区域由候选集合中的区域数量除以由确定的空间回收阶段的长度确定-XX:G1MixedGCCountTarget

如果 G1 预测在收集最小集合后还有时间剩余,则来自集合候选的额外老年代区域。添加老年代区域,直到预计使用剩余时间的 80%。

一组可选的收集集区域,G1 在其他两个部分被撤离后逐步撤离,并且在此暂停中还有时间。

前两组区域在初始收集过程中收集,可选收集集中的其他区域适合剩余的暂停时间。这种方法确保了空间回收的进度,同时由于对可选收集集的管理,提高了保持暂停时间和最小开销的概率。

当收集集候选区域集中没有剩余区域时,空间回收阶段结束。

定期垃圾收集

如果由于应用程序不活动而长时间没有进行垃圾收集,VM 可能会长时间持有大量未使用的内存。为避免这种情况,可以使用-XX:G1PeriodicGCInterval选项强制 G1 进行常规垃圾收集。此选项确定 G1 考虑执行垃圾回收的最小间隔(毫秒)。如果先前的垃圾收集暂停以来经过了这段时间,并且没有正在进行的并发循环,G1 会触发额外的垃圾收集,并可能产生以下影响:

在 Young-Only 阶段:G1 使用 Concurrent Start pause 开始并发标记,或者,如果-XX:-G1PeriodicGCInvokesConcurrent已指定,则使用 Full GC

在空间回收阶段:G1 继续空间回收阶段,触发适合当前进度的垃圾收集暂停类型。
-XX:G1PeriodicGCSystemLoadThreshold选项可用于细化是否触发垃圾收集:如果在getloadavg()JVM 主机系统(例如容器)上调用返回的平均一分钟系统负载值高于此值,则不会定期进行垃圾收集运行。

确定初始堆占用(Determining Initiating Heap Occupancy)

Initiating Heap Occupancy Percent (IHOP)是触发并发启动收集的阈值,它定义为老年代大小的百分比 。

默认情况下,G1 通过观察标记需要多长时间以及标记周期期间通常在老年代分配多少内存来自动确定最佳 IHOP。此功能称为自适应 IHOP。如果此功能处于活动状态,则选项-XX:InitiatingHeapOccupancyPercent将初始值确定为当前老年代大小的百分比,只要没有足够的观察值来对 Initiating Heap Occupancy 阈值进行良好预测。使用选项关闭 G1 的这种行为-XX:-G1UseAdaptiveIHOP。在这种情况下, -XX:InitiatingHeapOccupancyPercent决定这个阈值。

在内部,自适应 IHOP 尝试设置 Initiating Heap Occupancy,以便当老年代占用率达到当前最大老年代大小减去-XX:G1HeapReservePercent作为额外缓冲区的值时,空间回收阶段的第一次混合垃圾收集开始。

标记(Marking)

G1 标记使用一种称为Snapshot-At-The-Beginning (SATB)的算法。

它在初始标记暂停时拍摄堆的虚拟快照,此时在标记开始时处于活动状态的所有对象都被认为在剩余标记期间处于活动状态。这意味着在标记期间变为死(无法访问)的对象仍然被认为是活动的,以用于空间回收(有一些例外)。与其他收集器相比,这可能会导致一些额外的内存被错误地保留。但是,SATB 可能会在备注暂停期间提供更好的延迟。在该标记期间过于保守地考虑的活体将在下一次标记期间被回收。

在堆空间非常紧张情况下发生的行为

当应用程序保持大量内存以致疏散无法找到足够的空间复制时,就会发生疏散失败

疏散失败意味着 G1 试图通过将已经移动的对象保留在其新位置,而不复制任何尚未移动的对象,仅调整对象之间的引用来完成当前的垃圾回收。疏散失败可能会产生一些额外的开销,但通常应该与其他年轻收集(Young GC)一样快。在疏散失败的垃圾收集之后,G1 将正常恢复应用程序,无需任何其他措施。G1 会假设疏散失败发生在接近垃圾回收结束时。

如果这个假设不成立(没有足够内存执行应用程序),那么 G1 最终会安排一次 Full GC。这种类型的集合执行整个堆的就地压缩。这可能非常缓慢。

G1 尝试通过调度预防性年轻收集来避免年轻收集期间的疏散失败。假设是一个额外的不会导致疏散失败的常规年轻收集可能会通过回收巨大的区域来释放老一代中足够的内存,从而根本不会产生这种垃圾收集的开销。可以使用 -XX:-UsePreventiveGC option.

大对象(Humongous Objects)

大对象是大于或等于半个区域大小的对象。

这些大对象有时会以特殊方式处理:

每个大对象都被分配为老一代中的一系列连续区域。对象本身的起点始终位于该序列中第一个区域的起点。序列的最后一个区域中的任何剩余空间都将丢失用于分配,直到整个对象被回收。

一般来说,大对象只能在清理暂停期间标记结束时回收,或者在Full GC 期间。但是,对于原始类型数组(例如, bool、所有整数和浮点值)的大对象有一个特殊规定。如果大量对象在任意的垃圾回收暂停时没有被大量的对象引用,G1 机会主义地尝试回收巨大的对象。默认情况下启用此行为,但您可以使用选项禁用它-XX:G1EagerReclaimHumongousObjects

大对象的分配可能会导致垃圾收集暂停过早发生G1 在每个巨大的对象分配时检查初始堆占用阈值,如果当前占用超过该阈值,则可能立即强制初始标记年轻收集。

大对象从不移动,即使在 Full GC 期间也是如此。这可能会导致过早的缓慢 Full GC 或由于区域空间碎片而导致剩余大量可用空间的意外内存不足情况。

G1 GC默认参数

选项和默认值 描述

-XX:MaxGCPauseMillis=200

最大暂停时间的目标。

-XX:GCPauseTimeInterval= <ergo>

最大暂停时间间隔的目标。默认情况下,G1 不设置任何目标,允许 G1 在极端情况下连续执行垃圾回收。

-XX:ParallelGCThreads= <ergo>

垃圾收集暂停期间用于并行工作的最大线程数。这是从运行 VM 的计算机的可用线程数得出的,方法如下:如果进程可用的 CPU 线程数小于或等于 8,则使用该数。否则,将大于最终线程数的线程数增加八分之五。

-XX:HeapSizePerGCThread在每次暂停开始时,使用的最大线程数进一步受到最大总堆大小的限制:G1 不会使用每个Java 堆容量 超过一个线程。

-XX:ConcGCThreads= <ergo> 

用于并发工作的最大线程数。默认情况下,此值-XX:ParallelGCThreads 除以 4。

-XX:+G1UseAdaptiveIHOP

-XX:InitiatingHeapOccupancyPercent=45

控制初始堆占用率的默认值表示该值的自适应确定已打开,并且在前几个收集周期中,G1 将使用 45% 的老年代占用率作为标记开始阈值。

-XX:G1HeapRegionSize=<ergo> 

堆区域的大小。默认值基于最大堆大小,计算为渲染大约 2048 个区域,根据人体工程学确定的最大值为 32 MB。用户给定的大小必须是 2 的幂,有效值范围为 1 到 512 MB。

-XX:G1NewSizePercent=5

-XX:G1MaxNewSizePercent=60

年轻代的总大小,在这两个值之间变化,作为当前使用的 Java 堆的百分比。

-XX:G1HeapWastePercent=5

集合集合候选中允许的未回收空间百分比。如果候选集合中的可用空间低于此值,则 G1 停止空间回收阶段。

-XX:G1MixedGCCountTarget=8

集合中空间回收阶段的预期长度。

-XX:G1MixedGCLiveThresholdPercent=85

在此空间回收阶段不会收集活动对象占用率高于此百分比的老年代区域。

笔记:ergo表示实际值是根据环境以符合人体工程学的方式确定的。

在这里插入图片描述
点赞 收藏 关注

猜你喜欢

转载自blog.csdn.net/qq_35764295/article/details/126538045