1. Garbage First Overview

本文探讨了G1垃圾收集器如何通过区域划分、并行与并发机制优化内存管理和暂停时间,以及它如何适应大型堆和低延迟需求,对比了与Serial、Parallel和CMSGC的区别和优势。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本章将介绍 Garbage First(G1)垃圾收集器(GC),并从历史角度介绍 Java HotSpot 虚拟机(以下简称 HotSpot)中垃圾收集器,以及 G1 被纳入 HotSpot 中的原因。假定读者熟悉基本的垃圾收集概念,例如年轻代、年老代和压缩。 Java™ Performance 一书的第 3 章“JVM 概述”详细介绍了这些概念。

Serial GC 是 HotSpot 于 1999 年作为 Java Development Kit (JDK) 1.3.1 的一部分引入的第一个垃圾收集器。 Parallel 和 Concurrent Mark Sweep 收集器于 2002 年作为 JDK 1.4.2 的一部分引入。这三个收集器大致对应于三个最重要的 GC 用例:

  • Serial GC:最小化内存占用和并发开销
  • Parallel GC:最大化应用程序吞吐量
  • Concurrent Mark Sweep GC:最小化 GC 相关的暂停时间

有人可能会问,“为什么我们需要像 G1 这样的新垃圾收集器?”在回答之前,让我们澄清一些在比较和对比垃圾收集器时经常使用的术语。然后,我们将继续简要概述四个 HotSpot 垃圾收集器,包括 G1,并确定 G1 与其他垃圾收集器的不同之处。

术语

在本节中,我们定义术语并行【parallel】、停止世界【stop-the-world】和并发【concurrent】。术语并行【parallel】意味着多线程的垃圾收集操作。当 GC 事件活动【GC event activity】被描述为并行时,它会使用多个线程来执行它。当垃圾收集器被描述为并行时,它使用多个线程来执行垃圾收集。对于 HotSpot 垃圾收集器,几乎所有多线程 GC 操作都由内部 Java VM (JVM) 线程来处理。一个主要的例外是 G1 垃圾收集器,其中一些后台 GC 工作可以由应用程序线程承担。有关详细信息,请参阅第 2 章“深入理解 G1 收集器”和第 3 章“ G1 收集器性能调整”。

术语停止世界【stop-the-world】意味着所有 Java 应用程序线程都在 GC 事件【GC event】期间停止。 停止世界【stop-the-world】垃圾收集器是在执行垃圾收集时停止所有 Java 应用程序线程的垃圾收集器。 GC 阶段【GC phase】或事件【GC event】可以描述为停止世界【stop-the-world】,这意味着在特定的 GC 阶段GC phase】或事件【GC event】期间,所有 Java 应用程序线程都将停止。

术语并发【concurrent】意味着垃圾收集活动【garbage collection activity】在 Java 应用程序执行的同时发生。并发 GC 阶段【GC phase】或事件【GC event】意味着 GC 阶段【GC phase】或事件【GC event】与应用程序同时执行。

垃圾收集器可以用这三个术语中的任何一个或组合来描述。例如,一个并行并发收集器是多线程的(并行部分),并且还与应用程序同时执行(并发部分)。

Parallel GC

Parallel GC是一种并行的世界停止收集器,这意味着当 GC 发生时,它会停止所有应用程序线程,并使用多个线程执行 GC 工作。因此,GC 工作可以在没有任何中断的情况下非常有效地完成。相对于应用程序工作而言,这通常是最小化花费在 GC 工作上的最佳方法。但是,由 GC 引起的Java应用程序的个别暂停可能相当长。

Parallel GC 中的年轻代和老年代收集都是并行且停止世界的。老年代收集还执行压缩。压缩将对象靠得更近,以消除它们之间浪费的空间,从而实现最佳的堆布局。但是,压缩可能需要相当长的时间,这通常与 Java 堆的大小以及老年代活动对象的数量和大小有关。

在 HotSpot 中刚引入 Parallel GC 的时候,只有年轻代使用了并行停止世界收集器【parallel stop-the-world collector】。老年代收集器使用的是单线程停止世界收集器【single-threaded stop-the-world collector】。当 Parallel GC 首次引入时,在此配置中启用 Parallel GC 的 HotSpot 命令行选项是 -XX:+UseParallelGC。

在引入 Parallel GC 时,服务器的最常见用例需要优化吞吐量,因此 Parallel GC 成为 HotSpot Server VM 的默认收集器。此外,大多数 Java 堆的大小往往在 512MB 和 2GB 之间,这使得 Parallel GC 暂停时间相对较低,即使对于单线程停止世界收集【single-threaded stop-the-world collections】也是如此。同样在当时,延迟的要求往往比现在更宽松。 Web 应用程序通常可以容忍 GC 引起的延迟超过一秒,甚至三到五秒。

随着 Java 堆大小以及老年代中活动对象的数量和大小的增长,收集老年代的时间变得越来越长。与此同时,硬件的进步使更多的硬件线程变得可用。因此,通过添加一个多线程老年代收集器,与多线程年轻代收集器一起配合使用,Parallel GC 得到了极大的增强。这种增强的 Parallel GC 减少了收集和压缩堆所需的时间。

增强的 Parallel GC 是在 Java 6 更新版本中交付的。它是由一个名为 -XX:+UseParallelOldGC 的新命令行选项启用的。当 -XX:+UseParallelOldGC 被启用时,并行年轻代收集也被启用。这就是我们今天所认为的 HotSpot 中的 Parallel GC,一个多线程的停止世界的新生代收集器与一个多线程的停止世界的老年代收集器相结合。

TIP 在 Java 7 更新版本 4(也称为 Java 7u4 或 JDK 7u4)中,-XX:+UseParallelOldGC 被设为默认 GC 和 Parallel GC 的正常操作模式。从 Java 7u4 开始,指定 -XX:+UseParallelGC 也会启用 -XX:+UseParallelOldGC,同样指定 -XX:+UseParallelOldGC 也会启用 -XX:+UseParallelGC。

在以下用例中,并行 GC 是一个不错的选择:

1⃣️ 应用程序吞吐量要求比延迟要求更重要。  

批处理应用程序是一个很好的例子,因为它是非交互式的。当您开始批处理执行时,您希望它尽可能快地运行完成。

2⃣️ 如果可以满足最坏情况下的应用程序延迟要求,并行 GC 将提供最佳吞吐量。最坏情况下的延迟要求包括最坏情况下的暂停时间,以及暂停发生的频率。例如,应用程序可能有这样的延迟要求:“超过 500 毫秒的暂停每两小时不得超过一次,并且所有暂停不得超过三秒。”。

具有足够小的实时数据大小,以使并行 GC 的 full GC 事件能够满足或超过应用程序在最坏情况下由 GC 引起的延迟要求的交互式应用程序,是适合此用例的一个很好的示例。但是,由于实时数据量往往与 Java 堆的大小高度相关,因此属于此类别的应用程序类型是有限的。

并行 GC 适用于满足这些要求的应用程序。对于不满足这些要求的应用程序,暂停时间可能会变得过长,因为 full GC 必须标记整个 Java 堆并压缩老年代空间。因此,暂停时间往往会随着 Java 堆大小的增加而增加。

图 1.1 说明了 Java 应用程序线程(灰色箭头)是如何停止的,而 GC 线程(黑色箭头)是如何接管垃圾收集工作的。在此图中,有 8 个并行 GC 线程和 8 个 Java 应用程序线程,尽管在大多数应用程序中,应用程序线程的数量通常远超过 GC 线程的数量,尤其是在某些应用程序线程可能处于空闲状态的情况下。当发生 GC 时,所有应用程序线程都会停止,并且多个 GC 线程执行着 GC 过程。

Figure 1.1 How Java application threads are interrupted by GC threads when Parallel GC is used

Serial GC

串行 GC 与并行 GC 非常相似,只是它是在单个线程中完成所有工作。单线程方法允许不太复杂的 GC 实现,并且只需要很少的外部运行时数据结构。在内存占用方面,串行 GC 是所有 HotSpot 收集器中最低的。串行 GC 面临的挑战与并行 GC 类似。暂停时间可能很长,并且它们会随着堆大小和实时数据量而或多或少地线性增长。此外,使用串行 GC 时,长时间暂停会更加明显,因为 GC 工作是在单个线程中完成的。

由于内存占用低,串行 GC 是 Java HotSpot Client VM 上的默认设置。它还满足了许多嵌入式用例的要求。可以使用 -XX:+UseSerialGC HotSpot 命令行选项来明确指定使用串行 GC 。

图 1.2 说明了 Java 应用程序线程(灰色箭头)是如何停止的,而单个 GC 线程(黑色箭头)在运行 8 个 Java 应用程序线程的机器上接管,并执行垃圾收集工作。由于它是单线程的,因此在大多数情况下,串行 GC 执行 GC 事件所需的时间比并行 GC 更长,因为并行 GC 可以将 GC 工作分散到多个线程。

Figure 1.2 How Java application threads are interrupted by a single GC thread when Serial GC is used

Concurrent Mark Sweep (CMS) GC

CMS GC 的开发是为了响应越来越多的应用程序,这些应用程序要求 GC 的最坏情况暂停时间比串行或并行 GC 更短,并且可以接受牺牲一些应用程序吞吐量来消除或大大减少长时间的 GC 暂停次数。

在 CMS GC 中,年轻代垃圾收集与 Parallel GC 类似。它们是并行停止世界的,这意味着所有 Java 应用程序线程在年轻代垃圾收集期间都会暂停,并且垃圾收集工作由多个线程执行。请注意,您可以使用单线程年轻代收集器来配置 CMS GC,但此选项在 Java 8 中已被弃用,并在 Java 9 中被删除。

Parallel GC 和 CMS GC 的主要区别在于老年代收集。对于 CMS GC,老年代收集试图避免应用程序线程中的长时间停顿。为了实现这一点,CMS 老年代收集器的大部分工作与应用程序线程执行同时进行(即并发),除了一些相对较短的 GC 同步暂停【GC synchronization pauses】。 CMS 通常被称为大多数并发【mostly concurrent】,因为老年代收集的某些阶段还是会暂停应用程序线程。例如初始标记【initial-mark】和重新标记【remark】阶段。在 CMS 的初始实现中,初始标记【initial-mark】和重新标记【remark】阶段都是单线程的,但后来它们被增强为多线程。支持多线程初始标记【initial-mark】和重新标记【remark】阶段的 HotSpot 命令行选项是 -XX:+CMSParallelInitialMarkEnabled 和 -XX:CMSParallelRemarkEnabled。当 CMS GC 由 -XX:+UseConcurrentMarkSweepGC 命令行选项启用时,这些配置默认情况下会自动启用。

在进行老年代并发收集的同时,年轻代收集是可能发生的,而且很有可能发生。当这种情况发生时,老年代的并发收集会被年轻代的收集打断,并在年轻代收集完成后立即恢复。 CMS GC 的默认年轻代收集器通常称为 ParNew。

图 1.3 显示了在年轻代 GC(黑色箭头)、CMS初始标记【initial-mark】和重新标记【remark】阶段以及老年代 GC 停止世界阶段(也是黑色箭头),Java应用程序线程(灰色箭头)是如何停止的。CMS GC 中的老年代收集从一个停止世界的初始标记【initial-mark】阶段开始。初始标记完成后,并发标记【concurrent marking 】阶段就开始了,此时允许 Java 应用程序线程与 CMS 标记线程并发执行。在图 1.3 中,并发标记线程是前两个较长的黑色箭头,一个在“Marking/Pre-cleaning”标签的上方,另一个在其下方。一旦并发标记【concurrent marking 】完成,CMS 线程将执行并发预清理【concurrent pre-cleaning】,如“Marking/Pre-cleaning”标签下的两个较短的黑色箭头所示。注意,如果有足够的可用硬件线程,CMS 线程执行开销将不会对 Java 应用程序线程的性能产生很大影响。但是,如果硬件线程饱和或利用率很高,CMS线程将与Java应用程序线程争夺 CPU 周期。一旦并发预清理【concurrent pre-cleaning】完成,停止世界的重新标记【remark】阶段就开始了。重新标记【remark】阶段标记在初始标记之后以及并发标记和并发预清理执行时可能遗漏的对象。重新标记【remark】阶段完成后,开始并发清除【concurrent sweeping 】,释放所有死对象占用的空间。

Figure 1.3 How Java application threads are impacted by the GC threads when CMS is used

CMS GC 面临的挑战之一是对其进行调优,以便并发工作可以在应用程序用尽可用的 Java 堆空间之前完成。因此,关于 CMS 的一个棘手部分是找到合适的时间来开始并发工作。在处理相同的应用程序时,并发方法的一个常见结果是: CMS 通常需要比 Parallel GC 多 10% 到 20% 的 Java 堆空间。这是缩短 GC 暂停时间所付出的代价之一。

CMS GC 的另一个挑战是它如何处理老年代的碎片化。当老年代中的对象之间的空闲空间【free sapce】变得非常小或不存在,以致从年轻代提升的对象无法放入可用的空洞中【available hole】时,就会发生碎片化。 CMS 并发收集周期【CMS concurrent collection cycle】不执行压缩,甚至不执行增量或部分压缩。找不到可用的内存空洞,会导致 CMS 使用串行 GC 回退到 full GC,这通常会导致更长时间的暂停。与 CMS 中的碎片化相关的另一个不幸的挑战是它是不可预测的。一些应用程序运行可能永远不会经历由老年代碎片化导致的 full GC,而其他应用程序可能会经常经历它。

调优 CMS GC 可以帮助推迟碎片化,修改应用程序(例如避免大对象分配)也可以。调优可能是一项艰巨的任务,并且需要很多专业知识。修改应用程序以避免碎片化也可能具有挑战性。

收集器总结

到目前为止描述的所有收集器都有一些共同的问题。一是老年代收集器必须扫描整个老年代,以完成他们的大部分操作,例如标记、清理和压缩。这意味着执行工作的时间或多或少与 Java 堆大小呈线性关系。另一个是必须预先决定年轻代和老年代应该放置在虚拟地址空间中的哪个位置,因为年轻代和老年代是独立的连续内存块。

Garbage First (G1) GC

G1 垃圾收集器通过采用稍微不同的方法解决了 Parallel、Serial 和 CMS GC 的许多缺点。 G1 将堆按区域【region】划分。大多数 GC 操作可以一次在一个区域上执行,而不需要在整个 Java 堆或整个代上执行。

在 G1 中,年轻代只是一组区域,这意味着它不需要是一块连续的内存。同样,老年代也只是一组区域。无需在 JVM 启动时决定哪些区域应该属于老年代或年轻代。事实上,G1 的正常操作状态是随着时间的推移,映射到 G1 区域的虚拟内存在代之间来回移动。一个 G1 区域可能被指定为年轻区域,并且稍后,在年轻代收集之后,可以在其他地方使用,因为年轻代区域被完全疏散到未使用的区域。

在本章的其余部分,术语可用区域【available region】一词用于标识未使用且可供 G1 使用的区域。可用区域【available region】可以用作或指定为年轻代或年老代区域。有可能在年轻代收集之后,一个年轻代区域可以在未来的某个时间用作老年代区域。同样,在收集了一个老年代区域之后,它就变成了一个可用区域【available region】,可以在将来的某个时间用作年轻代区域。

G1 年轻代收集是并行的停止世界收集。如前所述,并行停止世界收集会在垃圾收集器线程执行时暂停所有 Java 应用程序线程,并且 GC 工作将分散在多个线程中。与其他 HotSpot 垃圾收集器一样,当发生年轻代收集时,会收集整个年轻代。

老年代 G1 收集器与其他 HotSpot 收集器的收集器完全不同。 G1 老年代收集不需要收集整个老年代来释放老年代中的空间。相反,每次都只能收集老年代区域的一个子集。此外,这个老年代区域子集是与年轻代收集一起收集的。

TIP 描述老年代区域子集的收集与年轻代收集一起的术语是 Mixed GC。因此,Mixed GC 是一个 GC 事件,其中除了老年代区域的子集之外,还收集所有年轻代区域。换句话说,Mixed GC 是正在收集的年轻代和老年代区域的混合。

与 CMS GC 类似,在一些可怕的情况下(比如老年代空间耗尽),有一个故障保护机制可以收集和压缩整个老年代。

G1 老年代收集是一组阶段组成,其中一些是并行的停止世界,一些是并行并发的。也就是说,一些阶段是多线程的并且停止所有应用程序线程,而其他阶段是多线程的并且与应用程序线程同时执行。第 2 章和第 3 章提供了每个阶段的更多详细信息。

当超过 Java 堆占用阈值时,G1 启动老年代收集。需要注意的是,G1 中的堆占用阈值衡量的是与整个 Java 堆相比的老年代占用率。熟悉 CMS GC 的读者记得,CMS 使用仅适用于老年代空间的占用阈值来启动老年代收集。在 G1 中,一旦达到或超过堆占用阈值,就会安排执行并行的停止世界的初始标记【initial-mark】阶段。

初始标记【initial-mark】阶段紧跟在下一次年轻代 GC 执行之后。初始标记【initial-mark】阶段完成后,将启动并发多线程标记【concurrent multithreaded marking phase】阶段以标记老年代中的所有活动对象。当并发标记【concurrent marking 】阶段完成时,将安排一个并行的停止世界的重新标记【remark】阶段来标记任何可能由于与并发标记阶段同时执行的应用程序线程而遗漏的对象。在重新标记【remark】结束时,G1 拥有关于老年代区域的完整标记信息。如果碰巧有老年代区域中没有任何活动对象,则可以在并发周期的下一个阶段(清理【cleanup】阶段)回收它们,而无需任何额外的 GC 工作。

同样在重新标记【remark】阶段结束时,G1 可以识别一组最佳可以用于收集的老年代。

TIP 垃圾收集期间要收集的区域集合称为收集集合(CSet)。

选择包含在 CSet 中的区域是基于可以释放的空间大小和 G1 暂停时间目标。在识别出 CSet 后,G1 会安排一次 GC 在接下来的几次年轻代 GC 期间收集 CSet 中的区域。也就是说,在接下来的几次年轻代 GC 中,除了年轻代之外,还会收集一部分老年代。这就是前面提到的 Mixed GC 类型的垃圾回收事件。  

使用 G1,每个被垃圾收集的区域,无论是年轻代还是老年代,都将其活动对象疏散到可用区域。一旦活动对象被疏散,已收集的年轻代和/或老年代区域将成为可用区域。  

将活动对象从老年代区域疏散到可用区域的一个有吸引力的结果是,被疏散的对象最终在虚拟地址空间中彼此相邻。对象之间没有碎片空间。实际上,G1 对老年代进行了部分压缩。请记住,CMS、Parallel 和 Serial GC 都需要 full GC 才能压缩老年代,并且压缩会扫描整个老年代。  

由于 G1 以每个区域为基础执行 GC 操作,因此适用于大型 Java 堆。即使 Java 堆大小可能相当大,GC 的工作量也可以限制在一小部分区域。  

G1 中暂停时间的最大贡献者是年轻代收集和混合收集,因此 G1 的设计目标之一是允许用户设置 GC 暂停时间目标。 G1 尝试通过 Java 堆的自适应大小来满足指定的暂停时间目标。它将根据暂停时间目标自动调整年轻代的大小和 Java 堆的总大小。暂停时间目标越低,年轻代越小,总堆就越大,这使得老年代相对较大。  

G1 的设计目标是将所需的调优限制为只需要设置最大 Java 堆大小和指定 GC 暂停时间目标。否则,G1 旨在使用内部启发式动态调​​整自身。在撰写本文时,G1 中的启发式是 HotSpot GC 开发中最热烈的地方。同样在撰写本文时,G1 在某些情况下可能需要额外的调整,但构建良好启发式的先决条件已经存在并且看起来很有希望。有关如何调优 G1 的建议,请参阅第 3 章。  

总而言之,G1 通过将 Java 堆拆分为多个区域,比其他垃圾收集器更适合大型 Java 堆。 G1 在部分压缩的帮助下处理 Java 堆碎片,它几乎以多线程方式完成所有工作。

在撰写本文时,G1 主要针对具有合理低暂停的大型 Java 堆的用例,以及那些使用 CMS GC 的应用程序。有计划使用 G1 也针对吞吐量用例,但是对于寻求可以容忍更长 GC 暂停的高吞吐量的应用程序,Parallel GC 目前是更好的选择。

G1 Design

如前所述,G1 将 Java 堆划分为多个区域。区域大小可以根据堆的大小而有所不同,但必须是 2 的幂,并且至少为 1MB,最多为 32MB。因此,可能的区域大小为 1、2、4、8、16 和 32MB。所有区域的大小都相同,并且它们的大小在 JVM 执行期间不会改变。区域大小计算基于初始和最大 Java 堆大小的平均值,因此该平均堆大小大约有 2000 个区域。例如,对于带有 -Xmx16g -Xms16g 命令行选项的 16GB Java 堆,G1 将选择 16GB/2000 = 8MB 的区域大小。  

如果初始和最大 Java 堆大小相距甚远,或者如果堆大小非常大,则可能有超过 2000 个区域。同样,一个小的堆大小最终可能会少于 2000 个区域。

每个区域都有一个关联的记忆集【remembered set 】(包含指向该区域的指针的位置集合,简称为 RSet)。 RSet 的总大小有限,但是很明显,所以区域的数量对HotSpot的内存占用有直接的影响。 RSet 的总大小很大程度上取决于应用程序的行为。在低端,RSet 开销约为堆大小的 1%,而在高端则为 20%。

 一个特定的区域一次仅用于一个目的,但是当该区域包含在一次收集中时,它将被完全疏散,并作为可用区域释放开来。

G1中有几种类型的区域。可用区域是指当前未使用的区域。伊甸区域构成年轻代 eden 空间,幸存区域构成年轻代 survivor 空间。所有伊甸区域和幸存区域的集合就是年轻代。伊甸区域或幸存区域的数量可以在一次次的年轻代 GC、混合 GC 【Mixed GC】或 full GC 之间变化。老年代区域包括大部分的老年代。最后,巨型区域被认为是老年代的一部分,其包含的对象大小为区域 50% 或更多的对象。在 JDK 8u40 更改之前,巨型区域被作为老年代的一部分来收集,但在 JDK 8u40 中,某些巨型区域被作为年轻代收集中的一部分。本章后面有更多关于巨型区域的细节。  

一个区域可以用于任何目的这一事实意味着不需要将堆划分为连续的年轻代和老年代段【segment】。相反,G1 启发式方法会估计年轻代可以包含多少个区域,并且仍可以在给定的 GC 暂停时间目标内收集完成。当应用程序开始分配对象时,G1 选择一个可用区域,将其指定为伊甸区域,并开始从该区域向 Java 线程分发内存块。一旦该区域已满,另一个未使用的区域被指定为伊甸区。该过程持续进行,直到达到最大的伊甸区域数,此时将启动年轻代 GC。

在年轻代 GC 期间,所有年轻代区域,包括伊甸区域和幸存区域都被收集。这些区域中的所有活动对象都被疏散到新的幸存区域或老年代区域。当当前疏散目标区域已满时,将会根据需要将可用区域标记为幸存者或老年代区域。  

当 GC 后老年代空间的占用率达到或超过启动堆占用阈值时,G1 将启动老年代收集。占用阈值由命令行选项 -XX:InitiatingHeapOccupancyPercent 控制,默认为 Java 堆的 45%。  

当标记阶段【marking】显示它们不包含活动对象时,G1 可以尽早回收老年代区域。此类区域被添加到可用区域集中。包含活动对象的旧区域计划包含在未来的 Mixed GC 中。

当标记【marking】阶段显示老年代区域不包含任何活动对象时,G1 可以提前回收老年代区域。此类区域将被添加到可用区域集中。包含活动对象的老年代区域根据计划被包含在未来的混合收集中。

 

G1 使用多个并发标记线程【concurrent marking threads】。为了避免从应用程序线程中窃取过多的 CPU,标记线程会以突发【bursts】方式完成它们的工作。它们在给定的时间段内完成尽可能多的工作,然后暂停一段时间,让 Java 线程执行。

Humongous Objects

G1 专门处理大型对象分配,或者 G1 所称的“巨型对象”。如前所述,巨型对象是占区域大小 50% 或更多的对象。该大小包括 Java 对象头。对象头大小在 32 位和 64 位 HotSpot VM 之间有所不同。可以使用 Java 对象布局工具(也称为 JOL)获取给定 HotSpot VM 中给定对象的头大小。截至撰写本文时,可以在 Internet 上找到 Java 对象布局工具。

当发生巨型对象分配时,G1 会找到一组连续的可用区域,这些区域加起来的内存足以容纳巨型对象。第一个区域被标记为“humongous start”区域,其他区域被标记为“humongous continues”区域。如果没有足够的连续可用区域,G1 将执行 full GC 来压缩 Java 堆。

巨型区域被视为老年代的一部分,但它们只包含一个对象。此属性允许 G1 在并发标记阶段检测到巨型区域不再处于活动状态时积极地收集该区域。发生这种情况时,可以一次回收包含巨型对象的所有区域。

G1 面临的一个潜在挑战是,寿命很短的巨型对象可能要等到它们不再被引用之后才会被回收。JDK 8u40 实现了一种方法,在某些情况下,可以在年轻收集期间回收巨型区域。在使用 G1 时,避免频繁分配巨型对象对于实现应用程序性能目标至关重要。JDK 8u40 中提供的增强功能有所帮助,但可能不是所有具有许多短暂生命周期巨型对象的应用程序的解决方案。

Full Garbage Collections

G1 中的 Full GC 使用与 Serial GC 收集器相同的算法实现。当发生 full GC 时,会执行整个 Java 堆的完全压缩。这可确保系统可以使用最大数量的可用内存。需要注意的是,G1 中的 full GC 是单线程的,因此可能会引入异常长的暂停时间。此外,G1 的设计使得不需要 full GC。 G1 有望在不需要 full GC 的情况下满足应用程序性能目标,并且通常可以通过调优化,而不需要 full GC。

Concurrent Cycle

一个 G1 并发周期包括多个阶段的活动:初始标记【initial marking】、并发根区域扫描【concurrent root region scanning】、并发标记【concurrent marking】、重新标记【remarking】和清理【cleanup】。并发周期的开始阶段是初始标记,结束阶段是清理。除清理阶段外,所有这些阶段都被认为是“标记活动对象图”的一部分。

初始标记【initial marking】阶段的目的是收集所有 GC Root。Root 是对象图的起点。要从应用程序线程收集 Root 引用,必须停止应用程序线程;因此初始标记【initial marking】阶段是停止世界的。在 G1 中,初始标记【initial marking】是作为年轻代 GC 暂停的一部分完成的,因为无论如何年轻代 GC 都必须收集所有 Roots。

标记操作还必须扫描并跟踪幸存区域中对象的所有引用。这就是并发根区域扫描【concurrent root region scanning】阶段所做的。在此阶段,所有 Java 线程都允许执行,因此不会发生应用程序暂停。唯一的限制是必须在允许下一次 GC 开始之前完成该扫描。原因是新的 GC 将生成一组全新的幸存者对象,这些对象可能与之前初始标记的幸存者对象不同。

大多数标记工作是在并发标记【concurrent marking】阶段完成的。多个线程合作标记活动对象图。允许所有 Java 线程与并发标记线程同时执行,因此应用程序没有暂停,尽管应用程序可能会遇到一些吞吐量降低。

并发标记完成后,需要另一个停止世界阶段来完成所有标记工作。这个阶段称为“重新标记阶段”,通常是一个非常短暂的停顿。

并发标记的最后一个阶段是清理阶段。在此阶段,发现不包含任何活动对象的区域将被直接回收。这些区域不会包含在年轻代或 Mixed GC 中,因为它们不包含活动对象。这些区域将会被添加到可用区域列表中。

标记阶段必须完成,以便找出哪些对象是活动的,以便就 Mixed GC 中包含哪些区域做出明智的决定。由于 Mixed GC 是 G1 中释放内存的主要机制,因此在 G1 用完可用区域之前完成标记阶段非常重要。如果在可用区域用完之前标记阶段没有完成,G1 将回退到 full GC 以释放内存。这是可靠的,但也是缓慢的。确保标记阶段及时完成以避免 full GC 可能需要调优,这将在第 3 章中详细介绍。

Heap Sizing

G1 中的 Java 堆大小始终是区域大小的倍数。除了这个限制,G1 可以像其他 HotSpot GC 一样在 -Xms 和 -Xmx 之间动态地增加和缩小堆大小。  

G1 可能出于以下几个原因增加 Java 堆大小:

  • 在 full GC 期间,根据堆大小计算可能会增加大小。
  • 当发生年轻代或 Mixed GC 时,G1 计算执行 GC 所花费的时间与执行 Java 应用程序所花费的时间。如果根据命令行设置 -XX:GCTimeRatio 在 GC 中花费的时间过多,则 Java 堆大小会增加。在这种情况下增加 Java 堆大小的想法是降低 GC 发生的频率,从而减少 GC 所花费的时间与执行应用程序所花费的时间相比。G1 中 -XX:GCTimeRatio 的默认值为 9。所有其他 HotSpot 垃圾收集器的默认值为 99。GCTimeRatio 的值越大,Java 堆大小的增加越激进。因此,其他 HotSpot 收集器在决定增加 Java 堆大小时更加积极,并且默认情况下,其目标是在 GC 上花费的时间相对于执行应用程序所花费的时间更少。
  • 如果对象分配失败,即使在完成 GC 之后,G1 也会尝试增加堆大小以满足对象分配,而不是立即回退到执行 full GC。

  • 如果一个巨型对象分配未能找到足够的连续空闲区域来分配对象,G1 将尝试扩展 Java 堆以获取更多可用区域,而不是进行一次 full GC。

  • 当 GC 请求一个新的区域来疏散对象时,G1 会更喜欢增加 Java 堆的大小来获得一个新的区域,而不是让 GC 失败并回退到一个 full GC 以试图找到一个可用的区域。

### Python 中 `gc.garbage` 的功能与使用方法 #### 什么是 `gc.garbage` 在 Python 的垃圾回收机制中,`gc.garbage` 是一个列表,用于存储无法被正常回收的对象。这些对象通常是由于循环引用而造成的内存泄漏[^1]。当垃圾回收器检测到某些对象存在循环引用,并且没有其他外部强引用指向它们时,如果这些对象无法通过 `__del__()` 方法安全销毁,则会被放入 `gc.garbage` 列表中。 #### 如何查看不可达对象 可以通过访问 `gc.garbage` 来检查程序运行过程中是否存在未处理的循环引用对象: ```python import gc # 查看当前存在的不可达对象 print(gc.garbage) ``` 此操作可以帮助开发者识别潜在的内存管理问题。 #### 清理 `gc.garbage` 中的内容 虽然 `gc.garbage` 存储的是无法自动清理的对象,但在特定情况下可以手动干预来尝试解决这些问题。例如,在确认不会引发副作用的前提下,可以直接清空该列表: ```python import gc # 手动触发一次垃圾回收 gc.collect() # 如果确定不需要保留 garbage 中的对象,可清除之 gc.garbage.clear() ``` 需要注意的是,这种做法可能带来风险,因为强行删除某些依赖关系复杂或者具有特殊析构逻辑的对象可能会导致程序崩溃或其他异常行为[^4]。 #### 使用场景举例 假设有一个类定义如下,其中包含了对自身的引用从而形成闭环结构: ```python class Node: def __init__(self, name): self.name = name self.parent = None def set_parent(self, parent_node): self.parent = parent_node node_a = Node('A') node_b = Node('B') node_a.set_parent(node_b) node_b.set_parent(node_a) del node_a del node_b ``` 上述代码片段创建了一个简单的双向链接节点树形结构实例之后立即删除了两个顶层变量名绑定。但由于两者的相互持有使得常规引用计数降为零变得不可能实现,所以即使调用了 `gc.collect()` ,这两个对象仍然会进入 `gc.garbage` 集合当中等待进一步分析或人为介入处理[^3]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值