G1垃圾收集器简介
垃圾优先 Garbage-First(G1)垃圾收集器面向多处理器机器,适用于大内存场景。它尝试在无需太多配置的情况下实现垃圾收集暂停时间目标,并同时实现高吞吐量。G1旨在通过适用于当前目标应用和环境的功能,提供最佳的延迟和吞吐量平衡,这些功能包括:
- - 堆大小高达数十 GB甚至更大,超过50% 的Java堆内存用于存储活动数据。
- - 对象分配和晋升的速率会随时间显著变化。
- - 堆中存在大量碎片化。
- - 具有可预测的暂停时间目标,不超过几百毫秒,避免长时间的垃圾回收暂停。
G1 在应用程序运行时同时执行部分工作,它会利用处理器资源来缩短收集暂停,这一点最为显著。这主要体现在应用程序运行时会有一个或多个垃圾回收线程处于活动状态。因此,与吞吐量收集器相比,尽管使用 G1 收集器时垃圾回收暂停通常要短得多,但应用程序的吞吐量也往往稍微降低。
G1 收集器通过后面讲到的几种方式实现高性能并努力实现暂停时间目标。
应用程序开启G1
垃圾优先(Garbage-First)垃圾收集器是默认的收集器,因此通常无需执行任何额外操作。你可以在命令行上显式启用它,提供参数 -XX:+UseG1GC。
基本概念
G1是一种分代、增量、并行、大部分并发、停顿式以及疏散式的垃圾收集器,它在每次STW暂停中监视暂停时间目标。类似于其他垃圾收集器,G1将堆分成(虚拟的)年轻代和老年代。空间回收工作主要集中在年轻代上,因为在该代进行空间回收是最为高效的,偶尔也会在老年代进行空间回收。
一些操作总是在STW暂停中执行,以改进吞吐量。而其他需要耗费更多时间的操作(如全堆操作,例如全局标记)则可以并行且与应用程序同时进行。为了缩短用于空间回收的停顿时间,G1采用了分步递增和并行的方式来执行空间回收。G1通过跟踪先前的应用程序行为和垃圾收集暂停信息来建立相关成本模型,以实现可预测性。它利用这些信息来确定在暂停期间执行的工作量大小。例如,G1首先在最为高效的区域(即大部分填充有效垃圾的区域)进行空间回收,这也是其名称来源之处。
G1主要通过 evacuate 疏散来回收空间:发现于选定内存区域中的存活对象被复制到新内存区域中,并在此过程中进行压缩。在完成疏散后,先前被存活对象占据的空间将被应用程序重新使用进行分配(通过写入新数据覆盖来替代删除操作)。
垃圾优先(Garbage-First)收集器不是实时垃圾收集器。它尝试在较长时间内以很高概率满足设定的暂停时间目标,但对于特定暂停,并不总是能绝对确定地满足这一目标。
堆的布局结构
G1将堆分成一组大小相等的堆区域,每个区域都是一段虚拟内存的连续范围,如图7-1所示。一个区域是内存分配和内存回收的单位。在任何给定时间,这些区域中的每一个可以是空的(浅灰色),或者被分配给特定的年代,年轻代或老年代。当内存请求到来时,内存管理器会分配空闲区域,并将其分配给一个年代,然后将它们作为可用空间返回给应用程序进行自我分配。
图7-1 G1垃圾收集器堆布局
年轻代包括伊甸园区域(红色)和幸存者区域(带有"S"的红色)。这些区域提供了与其他收集器中相应连续空间相同的功能,不同之处在于在G1中,这些区域通常以非连续的模式布置在内存中。老年代包括老年代区域(浅蓝色)。老年代区域可能是巨大对象所占用的大型区域(带有"H"的浅蓝色)。
应用程序总是首先分配给年轻代,也就是伊甸园区域,除了那些被直接分配到老年代的巨大对象。
垃圾收集器的回收周期
在高层级上,G1收集器在两个阶段之间进行交替。年轻代阶段包含垃圾收集,逐渐填充老年代中当前可用内存中的对象。空间回收阶段是G1在处理年轻代的同时逐步回收老年代中的空间。然后循环重新开始,进行下一个年轻代阶段。
图7-2概述了这一循环及可能发生的垃圾收集暂停顺序的示例:
图7-2垃圾收集周期概述
以下是G1垃圾收集循环的各个阶段和阶段间转换的详细描述:
1.年轻代阶段:这一阶段以少量普通的年轻代收集开始,将对象提升至老年代。当老年代占用达到一定阈值(初始堆占用率阈值)时,年轻代阶段和空间回收阶段之间的转换开始。此时,G1安排了一个并发初始 Young 收集(Concurrent Start young collection),而不是进行普通的年轻代收集。
2.并发初始化:这种类型的收集启动标记过程,并进行普通的年轻代收集。并发标记确定所有当前可达(存活)的老年代区域中需要在接下来的空间回收阶段中保留。在标记过程尚未完全完成时,可能会出现普通的年轻代收集。标记最终通过两个特殊的STW暂停完成:重新标记(Remark)和清理(Cleanup)。
3.重新标记:这个暂停最终化了标记本身,执行引用处理和类卸载,回收完全空的区域,并清理内部数据结构。在重新标记和清理之间,G1计算信息,以便稍后能够同时在选定的老年代区域中回收自由空间,在清理暂停中将会得以最终完成。
4.清理:这一暂停确定是否确实需要进行空间回收阶段。如果空间回收随后进行,年轻代阶段将以Prepare Mixed young collection 完成。
5.空间回收阶段:这一阶段包括多个普通 Young 收集,在这些普通 Young 收集之外,还会疏散掉一些存活对象所在的老年代区域。这些收集也被称为混合(Mixed)收集。当G1确定疏散更多老年代区域不会带来足够值得努力的自由空间时,空间回收阶段结束。
在进行空间回收后,垃圾回收循环重新开始进入另一个年轻代阶段。作为备用方案,在应用程序获取存活信息时如果出现内存耗尽情况,则G1将执行类似其他垃圾收集器那样的就地停顿式全堆压缩(Full GC)。
垃圾收集的暂停和集合
G1在停顿式暂停中执行垃圾收集和空间回收。通常,活动对象从源区域复制到堆中的一个或多个目标区域,同时会调整对这些移动对象的现有引用。
对于非巨大(non-humongous)区域,对象的目标区域根据该对象的源区域确定:
1.年轻代中的对象(伊甸园和幸存者区域)根据它们的年龄被复制到幸存者或老年代中。
2.老年代中的对象被复制到其他老年代区域中。
巨大(humongous)区域中的对象则被单独处理。G1只会确定它们的存活状态,如果它们是非存活对象,则回收它们占用的空间。G1只会在极少数情况下通过非常耗时的手段来移动巨大对象。
记忆集(Remembered Set)
为了疏散收集集合,G1管理了一个记忆集(remembered set):这是指包含指向收集集合的引用的收集之外的位置的集合。当收集集合中的对象在垃圾回收过程中移动时,来自收集之外对该对象的其他引用需要被修改,指向该对象的新位置。
记忆集条目表示用于节省内存的大致位置:通常,彼此靠近的引用会指向彼此附近的对象。G1在逻辑上将堆分成了卡片(card),默认情况下每个卡片大小为512字节。记忆集条目是这些卡片的压缩索引。
初始化时,G1基于每个区域进行管理记忆集:每个区域都包含一个区域级别的记忆集,即潜在具有对该区域引用关系的位置的集合。在垃圾回收过程中,整个收集设置的记忆集是从这些单独记忆容器生成出来。
记忆容器是通过懒加载创建的:在重新标记和清理暂停阶段之间,G1重新构建所有收集候选区域的记忆容器。除此之外,默认情况下对于 年期代 G1总是在每次回收时都进行更新;而对于一些巨型对象也是一样的。
收集集合(Collection Set)
收集集合是要从中回收空间的源区域的集合。根据垃圾回收的类型,收集集合包括不同类型的区域:
- 在仅年轻代(Young-Only)阶段,收集集合仅包括年轻代中的区域以及可能需要回收的巨大对象所在的巨大区域。
- 在空间回收(Space-Reclamation)阶段,收集集合包括年轻代中的区域、可能需要回收的巨大对象所在的巨大区域,以及来自收集集合候选区域集合中一些老年代区域。
收集候选区域是在空间回收阶段极有可能被回收的区域。G1在重新标记(Remark)暂停期间根据这些区域所包含的存活数据量及它们与其他区域之间的连通性来选择这些候选区域。G1更倾向于选择具有少量存活数据而非主要为存活数据所占据的区域,并且也更倾向于选择具有较少连通性而非高连通性的区域,因为对于这些更“高效”的区域进行回收所需的工作量较小。G1会将那些无法为释放内存贡献太多空间利益的候选区域剔除。这包括那些可以回收空间少于当前堆大小的-XX:G1HeapWastePercent百分比值设定值。G1将不会在这次空间回收阶段中对这些区域进行回收。
在重新标记与清理暂停期间,G1继续准备好这些被后续进行回收操作所需用到的工作,在清理暂停期结束时对它们按效率进行排序。同时,在随后进行混合(Mixed)回收时,会优先选择那些更高效、需要更少时间进行回收并且包含更多可用空闲空间的区域进行回收操作。
垃圾收集过程
垃圾回收包括四个阶段:
1.预清空收集集合(Pre Evacuate Collection Set)阶段:执行一些垃圾回收的准备工作,包括与mutator线程断开TLABs的连接,根据Java堆大小选择此次回收的收集集合,并进行其他一些小的准备工作。
2.合并堆根(Merge Heap Roots):G1从收集集合区域创建单一统一的记忆集,以便后续更容易并行处理。这样可以消除单独记忆集中的许多重复项,否则这些重复项将需要在后续以更昂贵的方式进行过滤。
3.清空收集集合(Evacuate Collection Set):主要工作发生在这个阶段。G1开始移动对象,从根引用开始。根引用是从收集集合外部来的引用,可以是来自某些VM内部数据结构(外部根)、代码(代码根),或者是来自Java堆剩余部分(堆根)。对于所有的根引用,G1将被引用到收集集合中的对象复制到它们应该移动到的目标位置,并处理它们对于收集集合中对象的引用作为新的根引用,直到没有更多的根引用。
可以使用`-Xlog:gc+phases=debug`日志记录观察每个阶段的时间信息,分别记录在Ext Root Scanning、Code Root Scan、Scan Heap Roots和Object Copy子阶段。
4.后清空收集集合(Post Evacuate Collection Set):包括参考处理和为接下来mutator阶段做准备等清理工作。
这些阶段对应着使用`-Xlog:gc+phases=info`日志记录打印出来的信息。
G1的详细设计
堆的大小
G1在调整Java堆大小时遵循标准规则,使用以下参数进行调整:
- -XX:InitialHeapSize作为初始Java堆大小
- -XX:MaxHeapSize作为最大Java堆大小
- -XX:MinHeapFreeRatio用于确定最小的空闲内存百分比
- -XX:MaxHeapFreeRatio用于确定调整后的最大空闲内存百分比
G1收集器在重新标记(Remark)和完全垃圾回收(Full GC)暂停期间考虑根据这些选项调整Java堆的大小。这个过程可能会向操作系统释放内存或者从操作系统分配内存。
堆扩展发生在收集暂停期间,而内存释放发生在应用程序并发暂停之后。
周期性的垃圾回收
如果由于应用程序的不活动导致长时间没有垃圾回收,虚拟机可能会长时间保留大量未使用的内存,这些内存本可以用在其他地方。为了避免这种情况,可以使用`-XX:G1PeriodicGCInterval`选项强制G1定期进行垃圾回收。该选项确定了G1考虑执行垃圾回收的最小时间间隔(以毫秒为单位)。如果距离上次垃圾回收暂停已经过去这么长时间,并且没有并发循环在进行中,G1会触发额外的垃圾回收,并可能产生以下效果:
在仅年轻代阶段:G1启动并发标记,使用并发启动暂停;或者如果指定了`-XX:-G1PeriodicGCInvokesConcurrent`选项,则会触发一次完全垃圾回收。
在空间回收阶段:G1会继续空间回收阶段,并触发适合当前进度的垃圾回收暂停类型。
`-XX:G1PeriodicGCSystemLoadThreshold`选项可以用来确定是否触发垃圾回收:如果JVM主机系统(例如容器)上getloadavg()调用返回的平均一分钟系统负载值高于此阈值,则不会运行定期的垃圾回收。
初始化堆的容量(IHOP)
初始堆占用百分比 Initiating Heap Occupancy Percent(IHOP)是触发并发启动收集的阈值,它被定义为老年代大小的百分比。
G1默认情况下通过观察标记所需的时间以及在标记周期中老年代通常分配了多少内存来自动确定最佳的IHOP。这个功能称为自适应IHOP。如果此功能处于活动状态,那么选项`-XX:InitiatingHeapOccupancyPercent`在没有足够观察来进行对初始堆占用百分比阈值进行良好预测时,该初始值作为当前老年代大小的百分比。关闭G1的这种行为可以使用选项`-XX:-G1UseAdaptiveIHOP`。在这种情况下,`-XX:InitiatingHeapOccupancyPercent`的值始终确定此阈值。
在内部,自适应IHOP试图设置初始堆占用百分比,以便空间回收阶段的第一次混合垃圾回收在老年代占用达到当前最大老年代大小减去`-XX:G1HeapReservePercent`作为额外缓冲区时开始。
标注
G1标记使用的算法称为Snapshot-At-The-Beginning(SATB)。它在初始标记暂停时对堆进行虚拟快照,这意味着在标记开始时存活的所有对象在其余的标记过程中都被认为是存活的。这意味着在标记过程中变为死亡(无法访问)的对象仍然被视为存活,供空间回收之用(有一些例外)。这可能会导致与其他收集器相比,错误地保留一些额外内存。然而,SATB在重新标记暂停期间可能提供更好的延迟性。被过度保守地考虑为存活的对象在下一个阶段的标记过程中将会被回收。
堆内的活跃对象过多处理
当应用程序保持了大量内存活跃,以至于无法找到足够的空间进行复制时,就会发生疏散失败(Evacuation Failure)。疏散失败意味着G1尝试通过将已经移动的对象保留在其新位置,而不是复制尚未移动的对象,并仅调整对象之间的引用来完成当前的垃圾回收。疏散失败可能会带来一些额外开销,但通常应该与其他年轻代收集一样快速。在疏散失败之后,G1将恢复应用程序运行,不会采取其他措施。G1假设疏散失败发生在垃圾回收结束之前;也就是说,大多数对象已经移动,并且有足够的剩余空间可以继续运行应用程序,直到标记完成并开始空间回收。
如果这个假设不成立,那么G1最终会安排进行一次完全垃圾回收(Full GC)。这种类型的收集对整个堆进行原地压缩,可能非常耗时。
巨型对象
巨型对象(Humongous objects)是指大小大于或等于半个区域的对象。当前的区域大小是根据G1 GC 的默认值,中描述的方式确定的,除非使用`-XX:G1HeapRegionSize`选项进行设置。
这些巨型对象有时会以特殊方式处理:
- - 每个巨型对象都会在老年代的一系列连续区域进行分配。对象本身的起始位置始终位于该序列中第一个区域的起始位置。在该序列中最后一个区域中剩余的空间,在整个对象被回收之前都无法用于分配。
- - 通常,巨型对象只能在清理暂停期间(即标记结束时)或者在进行了FGC后才能被回收。然而,对于例如布尔值、各种整数和浮点值等原始类型数组,有一项特殊规定。G1会尝试性的回收巨型对象,如果它们在任何垃圾回收暂停期间没有被许多对象引用。此行为默认启用,但您可以使用`-XX:G1EagerReclaimHumongousObjects`选项进行禁用。
- - 分配巨型对象可能会导致垃圾回收暂停过早发生。G1会在每次巨型对象分配时检查初始堆占用阈值,并且如果当前占用超过该阈值,则可能立即强制进行YGC。
- - 巨型对象只有在第一次FGC失败,进行第二次FGC中的阶段中,才会移动。这个过程非常缓慢。由于包含巨型对象末端的堆区域中存在无法使用的空间,因此仍然有可能导致G1以内存不足 (oom)退出虚拟机。
G1的默认配置
以下是G1的一些默认配置,无需显示的配置就可使用
Option and Default Value | Description |
---|---|
| 最大暂停时间 |
| 最大暂停时间间隔的目标。默认情况下,G1不设定任何目标,允许 G1在极端情况下连续执行垃圾收集。 |
| 垃圾回收暂停期间用于并行工作的最大线程数,取决于运行虚拟机的计算机上可用线程数量。计算方法如下:如果可用于进程的 CPU 线程数少于或等于 8,则使用该数量。否则,将大于 8 的线程数的五分之八添加到最终线程数中。 在每次暂停开始时,使用的最大线程数还受到最大总堆大小的限制:G1 不会使用超过每 -XX:HeapSizePerGCThread 个的 Java 堆容量一倍的线程。 |
| 并行工作的最大线程数。默认情况下,此值为-XX:ParallelGCThreads除以 4。 |
| 控制初始堆占用情况的默认值表明,自适应确定该值已经开启,并且在最初的几个收集周期中,G1 将使用老年代的 45% 作为标记启动阈值。 |
| 堆区域的大小。默认值基于最大堆大小,并且计算为大约 2048 个区域,最大符合人体工程学确定值为 32 MB。用户给定的大小必须是 2 的幂,并且有效的取值范围是从 1 到 512 MB。 |
| “young generation”的总大小是根据当前正在使用的Java堆大小按百分比而变化在这两个值之间。 |
| 允许在收集集合候选项中未回收的空间百分比。如果收集集合候选项中的可用空间低于这个值,G1将停止空间回收阶段。 |
| 空间回收阶段的预期长度以收集数量 |
| 在空间回收阶段中,老年代区域中活跃对象占用比例高于此百分比的区域将不会被收集。 |
注意: < ergo > 意味着实际值是根据环境的人机工程学确定的。
与其他收集器的比较
这是 G1 与其他收集器主要差异的摘要:
- 并行 GC 只能作为一个整体对老年代进行压缩和回收空间。而 G1 则将这项工作分配到多个更短的收集中,大大缩短了暂停时间,但有可能牺牲吞吐量。
- G1 在部分老年代空间回收时是并发进行的。
- 由于其并发特性,G1 的开销可能会比上述收集器更高,从而影响吞吐量。
- ZGC 旨在在牺牲一定吞吐量的情况下显著减少暂停时间。
由于其工作方式,G1 具有一些机制来提高垃圾回收效率:
- G1 可以在任何垃圾回收过程中回收老年代中一些完全空的大区域。这可以避免许很多本来不必要的垃圾回收,并且在不费力气的情况下释放大量空间。
- G1 还可以选择性地尝试并发地对 Java 堆中重复的字符串进行去重。
- 从老年代回收空的大对象始终是开启的。你可以使用选项 -XX:-G1EagerReclaimHumongousObjects 来禁用此特性。字符串去重默认情况下是禁用的,你可以使用选项 -XX:+G1EnableStringDeduplication 来启用它。