<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>概念 on Evorsio</title><link>https://blog.evorsio.com/tags/concept/</link><description>Recent content in 概念 on Evorsio</description><generator>Hugo -- gohugo.io</generator><language>zh</language><lastBuildDate>Tue, 28 Apr 2026 19:24:00 +1200</lastBuildDate><atom:link href="https://blog.evorsio.com/tags/concept/index.xml" rel="self" type="application/rss+xml"/><item><title>Z 垃圾收集器 (ZGC)</title><link>https://blog.evorsio.com/p/z-garbage-collector/</link><pubDate>Tue, 28 Apr 2026 19:24:00 +1200</pubDate><guid>https://blog.evorsio.com/p/z-garbage-collector/</guid><description>&lt;h1 id="z-垃圾收集器-zgc"&gt;Z 垃圾收集器 (ZGC)
&lt;/h1&gt;&lt;h2 id="动机"&gt;动机
&lt;/h2&gt;&lt;p&gt;ZGC（JEP 333）被设计用于低延迟和高可扩展性，并自 JDK 15（JEP 377）起已可用于生产环境。&lt;/p&gt;
&lt;p&gt;ZGC 的大部分工作是在应用线程运行时完成的，只会短暂地暂停这些线程。ZGC 的停顿时间通常以微秒计；相比之下，默认垃圾收集器 G1 的停顿时间范围从毫秒到秒级不等。ZGC 的低停顿时间与堆大小无关：工作负载可以使用从几百 MB 到数 TB 的堆大小，并仍然保持低停顿时间。&lt;/p&gt;
&lt;p&gt;对于许多工作负载而言，仅使用 ZGC 就足以解决所有与垃圾回收相关的延迟问题。这在具备足够资源（即内存和 CPU），以确保 ZGC 回收内存的速度快于并发运行的应用线程消耗内存的速度时效果良好。然而，ZGC 当前会将所有对象统一存储，不区分年龄，因此每次运行时都必须对所有对象进行回收。&lt;/p&gt;
&lt;p&gt;弱分代假说（weak generational hypothesis）指出：年轻对象往往很快死亡，而老对象则会长期存活。因此，回收年轻对象所需资源更少且能回收更多内存，而回收老对象则需要更多资源且回收的内存较少。因此，通过更频繁地回收年轻对象，可以提升使用 ZGC 的应用性能。&lt;/p&gt;
&lt;h2 id="描述"&gt;描述
&lt;/h2&gt;&lt;h3 id="启用分代-zgc"&gt;启用分代 ZGC
&lt;/h3&gt;&lt;p&gt;为了确保平稳过渡，我们最初将使分代 ZGC 与非分代 ZGC 并存。命令行选项 &lt;code&gt;-XX:+UseZGC&lt;/code&gt; 将选择非分代 ZGC；要选择分代 ZGC，需要额外添加 &lt;code&gt;-XX:+ZGenerational&lt;/code&gt; 选项：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-plain" data-lang="plain"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ java -XX:+UseZGC -XX:+ZGenerational ...
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;在未来的某个版本中，我们计划将分代 ZGC 设为默认值，此时 &lt;code&gt;-XX:-ZGenerational&lt;/code&gt; 将用于选择非分代 ZGC。在更晚的版本中，我们计划移除非分代 ZGC，此时 &lt;code&gt;ZGenerational&lt;/code&gt; 选项将被废弃。&lt;/p&gt;
&lt;h3 id="设计"&gt;设计
&lt;/h3&gt;&lt;p&gt;分代 ZGC 将堆划分为两个逻辑分代：年轻代用于存放新分配的对象，而老年代用于存放长期存活的对象。每个分代都独立于另一个分代进行回收，因此 ZGC 可以专注于回收更“高收益”的年轻对象。&lt;/p&gt;
&lt;p&gt;与非分代 ZGC 一样，所有垃圾回收都在应用程序运行的同时并发执行，并且应用程序的停顿时间通常小于一毫秒。由于 ZGC 在应用运行的同时读取和修改对象图，因此必须确保应用程序始终看到一致的对象图视图。ZGC 通过染色指针（colored pointers）、读屏障（load barriers）和写屏障（store barriers）来实现这一点。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;染色指针（colored pointer）&lt;/strong&gt; 是一种指向堆中对象的指针，它在包含对象内存地址的同时，还携带用于编码对象已知状态的元数据。这些元数据描述了对象是否已知存活、地址是否正确等信息。ZGC 始终使用 64 位对象指针，因此可以为多 TB 的堆提供足够的元数据位和地址空间。当对象字段引用另一个对象时，ZGC 使用染色指针来实现该引用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;读屏障（load barrier）&lt;/strong&gt; 是 ZGC 注入到应用程序中的一段代码，用于在应用读取对象字段（该字段引用另一个对象）时执行。读屏障会解析存储在染色指针中的元数据，并在应用使用该引用对象之前可能执行一些操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;非分代 ZGC 同时使用染色指针和读屏障。分代 ZGC 还额外使用写屏障，以高效地跟踪不同分代之间的引用。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;写屏障（store barrier）&lt;/strong&gt; 是 ZGC 注入到应用程序中的一段代码，用于在应用向对象字段写入引用时执行。分代 ZGC 在染色指针中增加了新的元数据位，使写屏障能够判断该字段是否已经被记录为可能包含跨分代引用。染色指针使得分代 ZGC 的写屏障比传统分代写屏障更高效。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;写屏障的引入，使分代 ZGC 可以将“可达对象标记”的工作从读屏障转移到写屏障中。也就是说，写屏障可以利用染色指针中的元数据位，高效判断在写入前该字段所引用的对象是否需要被标记。&lt;/p&gt;
&lt;p&gt;将标记（marking）从读屏障中移出，使得对其进行优化变得更容易，这一点非常重要，因为读屏障通常比写屏障执行得更频繁。现在，当读屏障解释一个染色指针时，如果对象已被搬迁，它只需要更新对象地址，并更新元数据以表示该地址已被确认正确。后续的读屏障会根据这些元数据进行判断，不再重复检查该对象是否已被搬迁。&lt;/p&gt;
&lt;p&gt;分代 ZGC 在染色指针中使用不同的标记与搬迁（relocation）元数据位集合，从而使各个分代可以独立进行回收。&lt;/p&gt;
&lt;p&gt;接下来的章节描述了区分分代 ZGC 与非分代 ZGC，以及与其他垃圾收集器的关键设计概念：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无多重内存映射（No multi-mapped memory）&lt;/li&gt;
&lt;li&gt;优化的屏障（Optimized barriers）&lt;/li&gt;
&lt;li&gt;双缓冲记忆集（Double-buffered remembered sets）&lt;/li&gt;
&lt;li&gt;无需额外堆内存的对象搬迁（Relocations without additional heap memory）&lt;/li&gt;
&lt;li&gt;紧凑堆区域（Dense heap regions）&lt;/li&gt;
&lt;li&gt;大对象（Large objects）&lt;/li&gt;
&lt;li&gt;完整垃圾回收（Full garbage collections）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="多重映射内存multi-mapped-memory"&gt;多重映射内存（multi-mapped memory）
&lt;/h3&gt;&lt;p&gt;非分代 ZGC 使用多重映射内存（multi-mapped memory）来降低读屏障的开销。而分代 ZGC 则改为在读屏障和写屏障中使用显式代码实现。&lt;/p&gt;
&lt;p&gt;对于用户而言，这一变化的主要优势在于：更容易衡量堆所使用的内存量。在多重映射情况下，同一段堆内存会被映射到三个独立的虚拟地址范围中，因此像 &lt;code&gt;ps&lt;/code&gt; 这样的工具报告的堆内存使用量大约是实际使用量的三倍。&lt;/p&gt;
&lt;p&gt;对于 GC 本身而言，这一变化意味着染色指针中的元数据位不再需要位于与堆可访问地址范围对应的指针部分。这使得可以增加更多元数据位，并且也为将最大堆大小从非分代 ZGC 的 16TB 限制进一步提升提供了可能性。&lt;/p&gt;
&lt;p&gt;在分代 ZGC 中，存储在对象字段中的对象引用以染色指针形式实现。而存储在 JVM 栈中的对象引用则以“无色指针”（colorless pointers）形式实现，即不包含元数据位，存在于硬件栈或 CPU 寄存器中。读屏障和写屏障负责在染色指针与无色指针之间进行相互转换。&lt;/p&gt;
&lt;p&gt;由于在硬件栈或 CPU 寄存器中不会出现染色指针，因此可以采用更特殊的染色指针布局，只要染色指针与无色指针之间的转换足够高效即可。分代 ZGC 使用的染色指针布局将元数据放在指针的低位，而对象地址放在高位。这样可以最大程度减少读屏障中的机器指令数量。在 x64 架构上，通过精心设计的编码方式，一个移位指令即可同时完成“判断是否需要处理该指针”以及“移除元数据位”的操作。&lt;/p&gt;
&lt;h3 id="优化屏障optimized-barriers"&gt;优化屏障（Optimized barriers）
&lt;/h3&gt;&lt;p&gt;随着写屏障（store barriers）的引入，以及读屏障（load barriers）新增的职责，更多 GC 代码将与编译后的应用代码交织在一起。为了最大化吞吐量，这些屏障需要被高度优化。分代 ZGC 的许多关键设计决策都涉及染色指针（colored pointer）方案和屏障机制。&lt;/p&gt;
&lt;p&gt;用于优化屏障的一些技术包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;快速路径与慢速路径&lt;/li&gt;
&lt;li&gt;最小化读屏障职责&lt;/li&gt;
&lt;li&gt;记忆集屏障（remembered-set barriers）&lt;/li&gt;
&lt;li&gt;SATB 标记屏障（SATB marking barriers）&lt;/li&gt;
&lt;li&gt;融合写屏障检查（fused store barrier checks）&lt;/li&gt;
&lt;li&gt;写屏障缓冲区（store barrier buffers）&lt;/li&gt;
&lt;li&gt;屏障补丁（barrier patching）&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="快速路径与慢速路径"&gt;快速路径与慢速路径
&lt;/h4&gt;&lt;p&gt;ZGC 将屏障拆分为两部分。快速路径检查在应用程序使用引用对象之前是否需要执行额外的 GC 工作。慢速路径执行这些额外工作。所有对象访问都会执行快速路径检查。由于必须非常快，这部分代码会直接插入到即时编译（JIT）后的应用程序代码中。慢速路径只在少数情况下执行。当进入慢速路径时，被访问对象指针的颜色会被修改，使得后续对同一指针的访问在一段时间内不会再次触发慢速路径。因此，慢速路径不需要高度优化，为了可维护性，它们以 JVM 中的 C++ 函数形式实现。&lt;/p&gt;
&lt;p&gt;在非分代 ZGC 中，这是读屏障（load barriers）的拆分方式。在分代 ZGC 中，同样的机制也应用于写屏障（store barriers）及其相关的 GC 工作。&lt;/p&gt;
&lt;h4 id="最小化读屏障load-barrier职责"&gt;最小化读屏障（load barrier）职责
&lt;/h4&gt;&lt;p&gt;在非分代 ZGC 中，读屏障负责：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;更新 GC 已搬迁对象的过期引用&lt;/li&gt;
&lt;li&gt;将加载的对象标记为存活（因为应用正在访问该对象，因此认为其存活）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在分代 ZGC 中，需要同时跟踪两个分代，并在染色指针（colored pointer）与无色指针之间进行转换。为了降低复杂度，并优化读屏障的快速路径，将标记职责从读屏障转移到写屏障。&lt;/p&gt;
&lt;p&gt;在分代 ZGC 中，读屏障负责：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;移除染色指针（colored pointer）中的元数据位&lt;/li&gt;
&lt;li&gt;更新 GC 已搬迁对象的过期引用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;写屏障（store barrier）负责：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;向指针添加元数据位以生成染色指针（colored pointer）&lt;/li&gt;
&lt;li&gt;维护记忆集（remembered set），用于跟踪老年代到年轻代的引用&lt;/li&gt;
&lt;li&gt;将对象标记为存活&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="记忆集屏障remembered-set-barriers"&gt;记忆集屏障（remembered-set barriers）
&lt;/h4&gt;&lt;p&gt;当分代 ZGC 回收年轻代时，只会访问年轻代中的对象。但老年代中的对象可能包含指向年轻代对象的引用，这些引用在回收过程中必须被处理，原因有两个：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GC 标记根（GC marking roots）：这些引用可能是使年轻代对象仍然可达的唯一路径，因此必须作为对象图的根来处理，以确保所有存活对象都能被标记&lt;/li&gt;
&lt;li&gt;过期引用：在回收年轻代时，对象会被移动，但老年代中的引用不会立即更新，而是在应用访问时由读屏障惰性更新。如果应用未访问这些引用，GC 需要在后续阶段统一修复这些过期引用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;老年代到年轻代的引用集合称为记忆集（remembered set）。记忆集包含所有位于老年代中的内存地址，这些地址可能包含指向年轻代对象的引用。写屏障（store barrier）会向记忆集添加条目。当一个引用被写入对象字段时，该字段被认为可能包含老年代到年轻代的引用。写屏障的慢速路径会过滤掉对年轻代字段的写入，因为只有老年代中的地址是需要关注的。慢速路径不会根据写入的值进行过滤，该值可能指向年轻代或老年代对象。垃圾收集器在使用记忆集时，会检查对象字段的当前值。&lt;/p&gt;
&lt;p&gt;这一切保证了写屏障（store barrier）在维护记忆集时具有“只执行一次”的特性。这意味着，在两个连续的年轻代标记阶段之间，每个被写入的对象字段，写屏障慢速路径只会执行一次。当某个字段第一次被写入时，会发生以下步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;快速路径检查即将被覆盖的字段原值&lt;/li&gt;
&lt;li&gt;颜色信息表明该字段在上一次年轻代标记阶段之后未被写入&lt;/li&gt;
&lt;li&gt;进入慢速路径&lt;/li&gt;
&lt;li&gt;将该字段地址加入记忆集（remembered set）&lt;/li&gt;
&lt;li&gt;将新的指针值染色后写入字段&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;新的指针值会被染色，使后续快速路径检查能够识别该字段已经执行过慢速路径处理。&lt;/p&gt;
&lt;h4 id="satb-标记屏障satb-marking-barriers"&gt;SATB 标记屏障（SATB marking barriers）
&lt;/h4&gt;&lt;p&gt;与非分代 ZGC 不同，分代 ZGC 使用起始快照（SATB，snapshot-at-the-beginning）标记算法。在标记阶段开始时，垃圾收集器会对 GC 根（GC roots）进行快照；在标记阶段结束时，所有在标记开始时从这些根可达的对象都能被保证找到并标记为存活。&lt;/p&gt;
&lt;p&gt;为实现这一点，当对象图中的引用关系被破坏时，垃圾收集器必须能够获知。因此，写屏障（store barrier）会将即将被覆盖的字段值上报给垃圾收集器；垃圾收集器随后会标记该引用对象，并继续访问并标记从该对象可达的其他对象。&lt;/p&gt;
&lt;p&gt;在一次标记周期内，写屏障只需要在某个字段第一次被写入时，上报该字段即将被覆盖的值。对同一字段的后续写入，只是替换一个垃圾收集器已经保证能够找到的值，因为 SATB 特性保证了这些对象在本次标记中已经被处理或会被处理。SATB 特性进一步支持了写屏障在标记语义上的“只执行一次”性质。&lt;/p&gt;
&lt;h4 id="融合写屏障检查fused-store-barrier-checks"&gt;融合写屏障检查（fused store barrier checks）
&lt;/h4&gt;&lt;p&gt;写屏障的记忆集维护和标记功能存在很多相似之处，它们都使用染色指针（colored pointer）的快速路径检查，并且都具有各自的“只执行一次”特性。因此，不再为每种条件设置独立的快速路径检查，而是将它们融合为一个统一的快速路径检查。如果任意一个条件不满足，就会进入慢速路径，并执行所需的 GC 工作。&lt;/p&gt;
&lt;h4 id="写屏障缓冲区store-barrier-buffers"&gt;写屏障缓冲区（store barrier buffers）
&lt;/h4&gt;&lt;p&gt;将屏障拆分为快速路径与慢速路径，并结合指针染色，可以减少对 C++ 慢速路径函数的调用次数。分代 ZGC 通过在快速路径与慢速路径之间引入一个由 JIT 编译的中间路径来进一步降低开销。该中间路径会将即将被覆盖的字段值以及对象字段地址存入写屏障缓冲区，然后直接返回到应用代码，而不会进入代价较高的慢速路径。只有当写屏障缓冲区被填满时，才会触发慢速路径。这种方式将从编译代码切换到 C++ 慢速路径的开销进行了摊销。&lt;/p&gt;
&lt;h4 id="屏障补丁barrier-patching"&gt;屏障补丁（barrier patching）
&lt;/h4&gt;&lt;p&gt;读屏障（load barrier）和写屏障（store barrier）在执行检查时，会访问垃圾收集器在不同阶段切换时更新的全局变量或线程本地变量。不同 CPU 架构在访问这些变量时的开销不同。&lt;/p&gt;
&lt;p&gt;在分代 ZGC 中，通过在可能的情况下对屏障代码进行补丁优化来降低这部分开销。全局状态值会被编码到屏障的机器指令中作为立即数，而不再需要通过访问全局变量或线程本地变量来获取当前状态。当垃圾收集器切换阶段时（例如开始年轻代标记阶段），相关方法首次执行时会对这些立即数进行更新。这进一步降低了屏障的运行开销。&lt;/p&gt;
&lt;h3 id="双缓冲记忆集double-buffered-remembered-sets"&gt;双缓冲记忆集（double-buffered remembered sets）
&lt;/h3&gt;&lt;p&gt;许多垃圾收集器使用一种称为卡表标记（card table marking）的记忆集技术来跟踪分代间引用。当应用线程写入对象字段时，也会同时将卡表（card table）中的某个字节标记为“脏”（dirty）。通常，卡表中一个字节对应堆中约 512 字节的地址范围。为了找到所有老年代到年轻代的引用，垃圾收集器必须定位并访问所有属于这些“脏字节”对应地址范围内的对象字段。&lt;/p&gt;
&lt;p&gt;相比之下，分代 ZGC 使用位图（bitmap）来精确记录对象字段位置，其中每一位表示一个可能的对象字段地址。每个老年代区域都有一对记忆集位图（remembered-set bitmaps）。其中一个位图由应用线程通过写屏障（store barrier）进行写入并保持活跃，另一个位图则作为垃圾收集器的只读副本，保存当前所有可能指向年轻代的老年代引用。这两个位图会在每次年轻代收集开始时进行原子交换。该机制的一个好处是应用线程不需要等待位图被清空。垃圾收集器可以在处理并清空一个位图的同时，另一个位图继续被应用线程并发写入。另一个好处是由于应用线程和 GC 线程操作不同位图，因此减少了它们之间对额外内存屏障（memory barrier）的需求。其他使用卡表标记的分代收集器（如 G1）在标记卡表时需要执行内存屏障，这可能导致写屏障性能更差。&lt;/p&gt;
&lt;h3 id="无需额外堆内存的对象迁移relocations-without-additional-heap-memory"&gt;无需额外堆内存的对象迁移（relocations without additional heap memory）
&lt;/h3&gt;&lt;p&gt;其他 HotSpot 垃圾收集器中的年轻代收集通常使用“转移式回收（scavenging）”模型：在一次遍历中找到并移动存活对象。在垃圾收集器完全知道哪些对象存活之前，年轻代中的所有对象都必须被处理完成。这类收集器只能在所有对象迁移完成后才能回收内存。因此，它们需要预估存活对象所需的内存量，并确保在 GC 开始时有足够空间。如果估算错误，就需要更昂贵的回收方式，例如对未迁移对象进行原地固定（pinning）导致碎片化，或暂停所有应用线程执行全堆 Full GC。&lt;/p&gt;
&lt;p&gt;分代 ZGC 使用两次遍历：第一次遍历用于访问并标记所有可达对象，第二次遍历用于迁移已标记对象。由于在迁移阶段开始之前 GC 已经拥有完整的存活信息，它可以按区域（region）粒度分配迁移工作。一旦某个区域内的所有存活对象都被迁移出去，该区域就被认为已完成回收，可以重新作为迁移目标区域或用于应用线程分配内存。即使没有新的空闲区域可用，ZGC 仍然可以通过将对象压缩到正在迁移的区域中继续执行。这使得分代 ZGC 可以在不依赖额外堆内存的情况下完成年轻代的迁移和压缩。&lt;/p&gt;
&lt;h3 id="密集堆区域dense-heap-regions"&gt;密集堆区域（dense heap regions）
&lt;/h3&gt;&lt;p&gt;当从年轻代迁移对象时，不同区域中的存活对象数量及其占用内存会存在差异。例如，最近分配的区域通常包含更多存活对象。&lt;/p&gt;
&lt;p&gt;ZGC 会分析年轻代区域的密度（density），以决定哪些区域值得回收，哪些区域过于“满”或回收成本过高。未被选择回收的区域会被原地晋升：其中的对象保持在原位置不移动，这些区域要么继续保留在年轻代作为幸存者区域（survivor regions），要么被晋升到老年代。这些幸存区域中的对象会获得“第二次死亡机会”，希望在下一次年轻代回收开始时，其中更多对象已经死亡，从而使更多区域可以被回收。&lt;/p&gt;
&lt;p&gt;这种对高密度区域进行原地老化（aging）的方式，可以降低年轻代回收的整体开销。&lt;/p&gt;
&lt;h3 id="大对象large-objects"&gt;大对象（large objects）
&lt;/h3&gt;&lt;p&gt;ZGC 已经能够很好地处理大对象。通过将虚拟内存与物理内存解耦，并预留大量虚拟地址空间，ZGC 通常可以避免 G1 中可能导致大对象分配困难的碎片问题。&lt;/p&gt;
&lt;p&gt;在分代 ZGC 中，这一点进一步增强：允许大对象直接在年轻代中分配。由于区域可以在不移动对象的情况下被老化，因此没有必要为了避免昂贵的迁移而将大对象强制分配到老年代。相反，如果大对象生命周期较短，可以在年轻代中直接回收；如果生命周期较长，也可以低成本地晋升到老年代。&lt;/p&gt;
&lt;h3 id="全垃圾收集full-garbage-collections"&gt;全垃圾收集（full garbage collections）
&lt;/h3&gt;&lt;p&gt;当回收老年代时，年轻代中的对象可能会引用老年代中的对象，这些引用被视为老年代对象图的根。由于年轻代对象变化频繁，不对年轻代到老年代的引用进行持续跟踪。相反，这些引用通过在老年代标记阶段同时执行一次年轻代回收来发现。当年轻代回收发现指向老年代的引用时，会将其传递给老年代标记过程。&lt;/p&gt;
&lt;p&gt;这次额外的年轻代回收仍然作为一次正常的年轻代回收执行，并会将存活对象保留在幸存区域中。这会带来一个影响：年轻代中的幸存对象不会参与老年代回收阶段中的引用处理和类卸载。因此，可能会出现这样的情况：应用释放了对象图的最后引用，调用系统级垃圾回收，然后期望某些弱引用被清除或入队，或者某些类被卸载，但实际上并没有立即发生。&lt;/p&gt;
&lt;p&gt;为了缓解这个问题，当应用显式触发垃圾回收时，会在老年代回收开始之前先执行一次额外的年轻代回收，将所有存活对象提前晋升到老年代。&lt;/p&gt;
&lt;h2 id="替代方案"&gt;替代方案
&lt;/h2&gt;&lt;h3 id="更简单的屏障与指针染色方案"&gt;更简单的屏障与指针染色方案
&lt;/h3&gt;&lt;p&gt;当前的读屏障（load barrier）和写屏障（store barrier）实现并不容易理解。一个更简单的版本可能更易维护，但代价是更高的读写屏障开销。我们评估了大约十种不同的屏障实现方式，但没有一种在性能上优于当前基于移位操作的读屏障设计。未来仍然值得继续研究并分析这种“性能与复杂度”之间的权衡。&lt;/p&gt;
&lt;h3 id="继续使用多重映射内存multi-mapped-memory"&gt;继续使用多重映射内存（multi-mapped memory）
&lt;/h3&gt;&lt;p&gt;可以通过继续使用多重映射内存（multi-mapped memory）来避免颜色指针中“无色根”的设计，从而采用更简单的实现方案。如果相比非分代 ZGC 需要更多的指针元数据位，那么最大堆大小将受到限制。另一种方式是采用混合方案：部分元数据位通过多重映射内存实现，另一部分则由读写屏障进行移除和添加。&lt;/p&gt;
&lt;h3 id="测试"&gt;测试
&lt;/h3&gt;&lt;p&gt;ZGC 实现中为无色指针（colorless pointer）和染色指针（colored pointer）使用了不同的 C++ 类型，从而保证两者之间不会发生隐式转换。染色指针仅限于 GC 代码和屏障使用。只要运行时系统通过 HotSpot 的访问接口和屏障来访问对象指针，它就只会看到可解引用的无色指针。运行时可见的对象指针类型始终是无色指针。我们在不同的对象指针类型中注入了大量验证代码，以便快速发现指针损坏或屏障缺失的问题。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;标准垃圾收集算法测试集将用于验证正确性。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="风险与假设"&gt;风险与假设
&lt;/h2&gt;&lt;h3 id="实现复杂度"&gt;实现复杂度
&lt;/h3&gt;&lt;p&gt;分代 ZGC（Generational ZGC）中的屏障和染色指针（colored pointer）比非分代 ZGC 更复杂。分代 ZGC 还会并发运行两个垃圾收集器；这两个收集器相对独立，但在一些复杂机制上仍然会相互交互，从而增加实现复杂度。&lt;/p&gt;
&lt;p&gt;由于额外的复杂性，从长期来看，我们计划通过完全用分代 ZGC 替代原有的非分代 ZGC，从而降低维护成本。&lt;/p&gt;
&lt;h3 id="分代-zgc-与非分代-zgc-的性能差异"&gt;分代 ZGC 与非分代 ZGC 的性能差异
&lt;/h3&gt;&lt;p&gt;我们认为分代 ZGC（Generational ZGC）比其前身更适合大多数使用场景。一些工作负载甚至可能由于资源使用更低而获得吞吐量提升。例如，在 Apache Cassandra 基准测试中，分代 ZGC 只需要四分之一的堆大小，却能达到四倍吞吐量，同时仍将停顿时间控制在 1 毫秒以内。&lt;/p&gt;
&lt;p&gt;某些本质上是非分代特性的工作负载可能会出现轻微性能下降。我们认为这类工作负载的比例足够小，不足以支撑长期维护两套独立 ZGC 实现的成本。&lt;/p&gt;
&lt;p&gt;另一个潜在开销来源是更复杂的 GC 屏障（GC barriers）。我们预计这部分开销大多会被避免频繁回收老年代对象所带来的收益抵消。&lt;/p&gt;
&lt;p&gt;另一个开销来源是同时运行两个垃圾收集器。需要合理平衡它们的触发频率和 CPU 使用，以避免对应用造成过度影响。&lt;/p&gt;
&lt;p&gt;与 GC 开发的常规情况一样，未来的改进和优化将由基准测试和用户反馈驱动。我们计划在首次发布之后仍持续改进分代 ZGC。&lt;/p&gt;</description></item><item><title>Garbage-First（G1）垃圾收集器</title><link>https://blog.evorsio.com/p/garbage-first-garbage-collector/</link><pubDate>Tue, 28 Apr 2026 18:24:00 +1200</pubDate><guid>https://blog.evorsio.com/p/garbage-first-garbage-collector/</guid><description>&lt;h1 id="garbage-firstg1垃圾收集器"&gt;Garbage-First（G1）垃圾收集器
&lt;/h1&gt;&lt;h2 id="简介"&gt;简介
&lt;/h2&gt;&lt;p&gt;Garbage-First（G1）垃圾收集器面向多处理器机器设计，可扩展到大容量内存。它力图在高概率下满足垃圾回收的停顿时间目标，同时在几乎无需配置的情况下实现高吞吐量。G1 旨在在当前目标应用和环境中，在延迟与吞吐量之间提供最佳平衡，这些应用和环境具有以下特征：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;堆大小可达数十 GB 或更大，且 Java 堆中超过 50% 被存活数据占用。&lt;/li&gt;
&lt;li&gt;对象分配和晋升（promotion）的速率会随时间显著变化。&lt;/li&gt;
&lt;li&gt;堆中存在大量内存碎片。&lt;/li&gt;
&lt;li&gt;需要可预测的停顿时间目标，且停顿时间不超过几百毫秒，从而避免长时间的垃圾回收暂停。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;G1 在应用程序运行的同时执行部分工作。它通过占用本可供应用程序使用的处理器资源，来换取更短的回收停顿时间。&lt;/p&gt;
&lt;p&gt;这一点最明显地体现在：当应用程序运行时，会有一个或多个垃圾回收线程同时处于活跃状态。因此，与以吞吐量为优先的收集器相比，虽然 G1 的垃圾回收停顿通常更短，但应用程序的整体吞吐量也往往会略有降低。&lt;/p&gt;
&lt;p&gt;G1 是默认的垃圾收集器。&lt;/p&gt;
&lt;p&gt;G1 收集器通过多种方式实现高性能，并尝试满足停顿时间目标，这些方式将在后续章节中进行说明。&lt;/p&gt;
&lt;h2 id="启用-g1"&gt;启用 G1
&lt;/h2&gt;&lt;p&gt;Garbage-First 垃圾收集器是默认收集器，因此通常不需要进行任何额外操作。你可以通过在命令行中添加 &lt;code&gt;-XX:+UseG1GC&lt;/code&gt; 来显式启用它。&lt;/p&gt;
&lt;h2 id="基本概念"&gt;基本概念
&lt;/h2&gt;&lt;p&gt;G1 是一种分代的、增量式的、并行的、主要是并发的、会产生 Stop-The-World（STW）暂停的、并通过回收（evacuating）方式工作的垃圾收集器，它会在每次 STW 暂停中监控暂停时间目标。与其他收集器类似，G1 将堆划分为（虚拟的）新生代和老年代。空间回收主要集中在新生代，因为在该区域进行回收效率最高，同时也会偶尔在老年代进行空间回收。&lt;/p&gt;
&lt;p&gt;某些操作始终在 Stop-The-World 暂停中执行，以提升吞吐量。而一些在应用程序停止时会耗时较长的操作，例如整个堆级别的操作（如全局标记），则会与应用程序并行、并发执行。为了在空间回收过程中保持较短的 STW 暂停，G1 以增量方式、分步骤并行地执行空间回收。G1 通过跟踪之前应用行为和垃圾回收暂停的信息来建立成本模型，从而实现可预测性，并使用这些信息来决定每次暂停中执行的工作量。例如，G1 会优先回收最有效率的区域（即主要充满垃圾的区域，因此得名 Garbage-First）。&lt;/p&gt;
&lt;p&gt;G1 主要通过“疏散（evacuation）”来回收空间：将选定内存区域中的存活对象复制到新的内存区域中，并在此过程中对其进行压缩。在疏散完成后，原先被存活对象占用的空间会被应用程序重新用于分配。&lt;/p&gt;
&lt;p&gt;Garbage-First 收集器不是实时收集器。它试图在较长时间范围内以较高概率满足设定的暂停时间目标，但对于单次暂停来说，并不能保证绝对确定地满足该目标。&lt;/p&gt;
&lt;h3 id="堆布局"&gt;堆布局
&lt;/h3&gt;&lt;p&gt;G1 将堆划分为一组大小相等的堆区域（heap regions），每个区域都是一段连续的虚拟内存范围，如图 7-1 所示。区域是内存分配和内存回收的基本单位。在任何给定时刻，这些区域可以是空的（浅灰色），或者被分配给某个特定的分代：新生代或老年代。当有内存请求到来时，内存管理器会分配空闲区域，并将其分配给某个分代，然后将其作为可用空间返回给应用程序，供其进行分配。&lt;/p&gt;
&lt;p&gt;&lt;img alt="G1 垃圾收集器堆布局" class="gallery-image" data-flex-basis="333px" data-flex-grow="139" height="341" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog.evorsio.com/p/garbage-first-garbage-collector/grbgcltncyl.png" width="474"&gt;&lt;/p&gt;
&lt;p&gt;新生代包含 Eden 区域（红色）和 Survivor 区域（带 “S” 的红色）。这些区域的功能与其他收集器中的对应连续空间相同，但不同之处在于，在 G1 中这些区域在内存中通常以非连续的方式分布。老年代区域（浅蓝色）构成老年代。对于跨越多个区域的对象，老年代区域可能是巨型对象（humongous）（带 “H”的浅蓝色）。&lt;/p&gt;
&lt;p&gt;应用程序始终在新生代中进行分配，也就是 Eden 区域中进行分配，但“巨型对象”除外，这类对象会直接在老年代中分配。&lt;/p&gt;
&lt;h3 id="垃圾收集周期"&gt;垃圾收集周期
&lt;/h3&gt;&lt;p&gt;从高层次来看，G1 收集器在两个阶段之间交替运行。仅年轻代阶段（young-only phase）包含垃圾回收操作，这些操作会逐步将当前可用内存填充为老年代中的对象。空间回收阶段（space-reclamation phase）则是在处理年轻代的同时，逐步回收老年代中的空间。随后，该周期会再次从仅年轻代阶段开始。&lt;/p&gt;
&lt;p&gt;&lt;img alt="垃圾收集周期概览" class="gallery-image" data-flex-basis="357px" data-flex-grow="149" height="300" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog.evorsio.com/p/garbage-first-garbage-collector/jgrbg_frst_hp.png" width="447"&gt;&lt;/p&gt;
&lt;p&gt;以下列表详细描述了 G1 垃圾收集周期的各个阶段、它们的停顿以及阶段之间的转换过程：&lt;/p&gt;
&lt;h4 id="仅年轻代阶段young-only-phase"&gt;&lt;strong&gt;仅年轻代阶段（Young-only phase）&lt;/strong&gt;
&lt;/h4&gt;&lt;p&gt;该阶段从若干次“普通年轻代收集（Normal young collections）”开始，这些收集会将对象逐步晋升到老年代。当老年代的占用率达到某个阈值（即“堆占用触发阈值 / Initiating Heap Occupancy threshold”）时，系统会从该阶段过渡到空间回收阶段（space-reclamation phase）。此时，G1 会调度一次“并发开始收集（Concurrent Start young collection）”，而不是普通的年轻代收集。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Concurrent Start（并发开始）&lt;/strong&gt;：
这种收集在执行普通年轻代收集的同时，还会启动标记（marking）过程。并发标记用于确定老年代区域中当前所有可达（存活）的对象，这些对象将在后续的空间回收阶段中被保留。
在标记尚未完全结束时，仍然可能发生普通的年轻代收集。标记最终通过两个特殊的 Stop-The-World（STW）停顿完成：Remark 和 Cleanup。&lt;/p&gt;
&lt;p&gt;Concurrent Start 停顿也可能判断不需要继续进行标记：在这种情况下，会发生一个简短的并发标记撤销（undo）阶段，然后继续仅年轻代阶段。在这种情况下，不会发生 Remark 和 Cleanup 停顿。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Remark（重新标记）&lt;/strong&gt;：
该停顿用于最终完成标记本身，并执行引用处理和类卸载，回收完全空的区域，并清理内部数据结构。在 Remark 和 Cleanup 之间，G1 会计算后续在并发过程中回收选定老年代区域空间所需的信息，这些工作将在 Cleanup 阶段最终完成。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Cleanup（清理）&lt;/strong&gt;：
该停顿用于判断是否真的进入空间回收阶段。如果进入空间回收阶段，则仅年轻代阶段会以一次“准备混合收集（Prepare Mixed young collection）”结束。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="空间回收阶段space-reclamation-phase"&gt;&lt;strong&gt;空间回收阶段（Space-reclamation phase）&lt;/strong&gt;
&lt;/h4&gt;&lt;p&gt;该阶段由多次年轻代收集组成，这些收集除了处理年轻代区域外，还会回收部分老年代区域中的存活对象。这些收集也称为“混合收集（Mixed collections）”。当 G1 判断继续回收更多老年代区域所带来的收益不足以抵消成本时，空间回收阶段结束。&lt;/p&gt;
&lt;p&gt;在空间回收结束后，整个收集周期会重新回到新的仅年轻代阶段作为开始。&lt;/p&gt;
&lt;p&gt;作为备用机制，如果在收集存活信息的过程中应用程序耗尽内存，G1 会像其他收集器一样执行一次原地的 Stop-The-World 全堆压缩（Full GC）。&lt;/p&gt;
&lt;h3 id="垃圾收集停顿与收集集合collection-set"&gt;垃圾收集停顿与收集集合（Collection Set）
&lt;/h3&gt;&lt;p&gt;G1 在 Stop-The-World（STW）停顿中执行垃圾收集和空间回收操作。存活对象通常会从源区域复制到堆中的一个或多个目标区域，同时对这些被移动对象的引用进行调整。&lt;/p&gt;
&lt;p&gt;对于非巨型（non-humongous）区域，一个对象的目标区域是根据该对象所在的源区域来确定的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;新生代（年轻代）的对象（eden 和 survivor 区域中的对象）会根据其年龄被复制到 survivor 区域或老年代区域。&lt;/li&gt;
&lt;li&gt;来自老年代区域的对象会被复制到其他老年代区域。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;位于巨型（humongous）区域中的对象则以不同方式处理。G1 只会判断这些对象是否存活，如果不存活，则回收其占用的空间。G1 只有在非常慢的“最后手段”收集过程中才会移动巨型对象。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Remembered Set（记忆集）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;为了对收集集合（collection set）中的区域进行疏散（evacuation），G1 维护了一个记忆集（remembered set）：它记录了堆中&lt;strong&gt;位于收集集合之外、但包含指向收集集合内部引用的位置集合&lt;/strong&gt;。当垃圾回收期间收集集合中的对象被移动时，所有来自收集集合外部、指向该对象的引用都必须被更新为指向对象的新位置。&lt;/p&gt;
&lt;p&gt;记忆集条目使用近似位置来节省内存：通常来说，彼此接近的引用往往指向彼此接近的对象。G1 在逻辑上将堆划分为卡片（card），默认大小为 512 字节。记忆集条目是这些卡片索引的压缩表示。&lt;/p&gt;
&lt;p&gt;G1 最初以“按区域（per-region）”的方式管理记忆集：每个区域都包含一个本地区域的记忆集，即可能包含指向该区域引用的位置集合。在垃圾回收期间，会基于这些区域记忆集生成整个收集集合的记忆集。&lt;/p&gt;
&lt;p&gt;记忆集主要是延迟创建的：在 Remark 与 Cleanup 停顿之间，G1 会为所有收集集合候选区域重建其记忆集。除此之外，G1 始终维护年轻代区域的记忆集，因为它们在每次回收中都会被处理，并且默认情况下也会对部分巨型对象进行维护，以便进行主动回收。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Collection Set（收集集合）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;收集集合是指用于回收空间的源区域集合。根据垃圾收集类型的不同，收集集合由不同类型的区域组成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在仅年轻代阶段（Young-Only phase）中，收集集合仅包含年轻代区域，以及可能可以回收的巨型对象所在区域。&lt;/li&gt;
&lt;li&gt;在空间回收阶段（Space-Reclamation phase）中，收集集合包含年轻代区域、可能可回收的巨型对象区域，以及来自候选集合中的部分老年代区域。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;收集集合候选区域（collection set candidate regions）是那些在空间回收阶段中&lt;strong&gt;高度可能被回收&lt;/strong&gt;的区域。G1 会在 Remark 停顿期间根据它们包含的存活数据量以及与其他区域的连接关系来选择这些区域。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;存活数据少（即空闲空间多）的区域优先于大部分数据存活的区域&lt;/li&gt;
&lt;li&gt;连接性低的区域优先于连接性高的区域&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因为回收这些“更高效”的区域所需的工作量更小。G1 会从候选区域中剔除那些对可回收空间贡献不大的区域，例如那些可回收空间少于 &lt;code&gt;-XX:G1HeapWastePercent&lt;/code&gt; 指定比例的区域。这些区域在本次空间回收阶段中将不会被回收。&lt;/p&gt;
&lt;p&gt;在 Remark 与 Cleanup 停顿之间，G1 会开始为后续回收做准备，而 Cleanup 停顿则完成这些工作，并根据效率对区域进行排序。那些回收成本更低、同时可释放空间更多的区域，会在后续的 Mixed 收集中被优先回收。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;垃圾收集过程（Garbage Collection Process）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;一次垃圾收集由四个阶段组成：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Pre Evacuate Collection Set（疏散前准备阶段）
该阶段执行垃圾收集的一些准备工作：将 TLAB（线程本地分配缓冲区）从 mutator 线程中分离、根据《Java 堆大小调整》中描述的方式选择本次收集集合（collection set），以及其他一些较小的准备操作。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Merge Heap Roots（合并堆根）
在该阶段，G1 会从收集集合区域中创建一个统一的记忆集（remembered set），以便后续进行更容易的并行处理。这一步会去除各个独立记忆集中大量重复项，否则这些重复项需要在之后以更高成本进行过滤处理。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Evacuate Collection Set（疏散收集集合）
这是主要的工作阶段：G1 从根（roots）开始移动对象。根引用是指来自收集集合之外的引用，包括：&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;JVM 内部数据结构中的引用（外部根 external roots）&lt;/li&gt;
&lt;li&gt;代码中的引用（代码根 code roots）&lt;/li&gt;
&lt;li&gt;Java 堆其余部分中的引用（堆根 heap roots）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于所有根引用，G1 会将收集集合中被引用的对象复制到其目标区域，并递归处理其引用，将指向收集集合内对象的引用作为新的根继续处理，直到不再存在新的根为止。&lt;/p&gt;
&lt;p&gt;各个子阶段的执行时间可以通过 &lt;code&gt;-Xlog:gc+phases=debug&lt;/code&gt; 日志观察，包括：Ext Root Scanning、Code Root Scan、Scan Heap Roots 和 Object Copy 等子阶段。&lt;/p&gt;
&lt;p&gt;G1 还可能会为可选的收集集合重复执行主要的疏散阶段。&lt;/p&gt;
&lt;ol start="4"&gt;
&lt;li&gt;Post Evacuate Collection Set（疏散后处理阶段）
该阶段包含清理工作，包括引用处理，以及为后续 mutator（应用线程）阶段做准备。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这些阶段对应于使用 &lt;code&gt;-Xlog:gc+phases=info&lt;/code&gt; 时所打印的各个阶段。&lt;/p&gt;
&lt;h2 id="garbage-first-内部机制garbage-first-internals"&gt;Garbage-First 内部机制（Garbage-First Internals）
&lt;/h2&gt;&lt;p&gt;本节描述 Garbage-First（G1）垃圾收集器的一些重要细节。&lt;/p&gt;
&lt;h3 id="java-堆大小调整java-heap-sizing"&gt;Java 堆大小调整（Java Heap Sizing）
&lt;/h3&gt;&lt;p&gt;G1 在调整 Java 堆大小时遵循标准规则，使用以下参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-XX:InitialHeapSize&lt;/code&gt;：Java 堆初始大小&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-XX:MaxHeapSize&lt;/code&gt;：Java 堆最大大小&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-XX:MinHeapFreeRatio&lt;/code&gt;：用于确定最小空闲内存百分比&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-XX:MaxHeapFreeRatio&lt;/code&gt;：用于确定调整后最大空闲内存百分比&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;G1 垃圾收集器仅在 &lt;strong&gt;Remark（重新标记）停顿&lt;/strong&gt; 和 &lt;strong&gt;Full GC（完整垃圾回收）停顿&lt;/strong&gt;期间考虑根据这些选项调整 Java 堆大小。该过程可能会向操作系统释放内存，或从操作系统申请内存。&lt;/p&gt;
&lt;p&gt;堆扩展发生在垃圾收集停顿期间，而内存释放则发生在停顿之后，并与应用程序并发执行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;仅年轻代阶段的分代大小调整（Young-Only Phase Generation Sizing）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;G1 总是在一次普通的年轻代收集结束时，为下一个 mutator（应用线程）阶段确定年轻代的大小。通过这种方式，G1 可以基于对实际停顿时间的长期观察，满足使用 &lt;code&gt;-XX:MaxGCPauseTimeMillis&lt;/code&gt; 和 &lt;code&gt;-XX:GCPauseIntervalMillis&lt;/code&gt; 设置的停顿时间目标。&lt;/p&gt;
&lt;p&gt;该计算会考虑相似大小的年轻代在以往回收中所花费的时间，例如包括以下信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在垃圾收集期间需要复制的对象数量&lt;/li&gt;
&lt;li&gt;这些对象之间的相互关联程度&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;-XX:GCPauseIntervalMillis&lt;/code&gt; 和 &lt;code&gt;-XX:MaxGCPauseTimeMillis&lt;/code&gt; 选项共同定义了最小应用程序利用率（Minimum Mutator Utilization, MMU）。G1 会尝试在每一个长度为 &lt;code&gt;-XX:GCPauseIntervalMillis&lt;/code&gt; 的时间窗口内，使垃圾收集停顿时间最多占用 &lt;code&gt;-XX:MaxGCPauseTimeMillis&lt;/code&gt; 毫秒。&lt;/p&gt;
&lt;p&gt;如果没有其他约束，G1 会在 &lt;code&gt;-XX:G1NewSizePercent&lt;/code&gt; 和 &lt;code&gt;-XX:G1MaxNewSizePercent&lt;/code&gt; 所定义的范围之间自适应地调整年轻代大小，以满足停顿时间目标。有关如何修复长时间停顿的更多信息，请参见《Garbage-First 垃圾收集器调优》。&lt;/p&gt;
&lt;p&gt;另外，也可以使用 &lt;code&gt;-XX:NewSize&lt;/code&gt; 与 &lt;code&gt;-XX:MaxNewSize&lt;/code&gt; 分别设置年轻代的最小和最大大小。&lt;/p&gt;
&lt;blockquote class="alert alert-note"&gt;
 &lt;div class="alert-header"&gt;
 &lt;span class="alert-icon"&gt;📝&lt;/span&gt;
 &lt;span class="alert-title"&gt;备注&lt;/span&gt;
 &lt;/div&gt;
 &lt;div class="alert-body"&gt;
 &lt;p&gt;只指定其中一个后续选项（-XX:NewSize 或 -XX:MaxNewSize）会将年轻代大小固定为通过该参数传入的精确值。&lt;/p&gt;
&lt;p&gt;这会禁用停顿时间控制（pause time control）。&lt;/p&gt;
 &lt;/div&gt;
 &lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;空间回收阶段的分代大小调整（Space-Reclamation Phase Generation Sizing）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在空间回收阶段（space-reclamation phase）中，G1 会尝试在单次垃圾收集停顿中，最大化老年代空间的回收量。年轻代的大小通常会被设置为允许的最小值，一般由 &lt;code&gt;-XX:G1NewSizePercent&lt;/code&gt; 决定，同时也会考虑 MMU（最小应用程序利用率）规范。&lt;/p&gt;
&lt;p&gt;在该阶段的每一次 mixed 收集开始时，G1 会从收集集合候选区域中选择一部分区域加入到收集集合中。这些额外加入的老年代区域由三部分组成：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;最小老年代区域集合
用于确保疏散（evacuation）进度的基本集合。该集合的大小由“收集集合候选区域数量 ÷ 空间回收阶段长度（由 &lt;code&gt;-XX:G1MixedGCCountTarget&lt;/code&gt; 决定）”来计算。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;额外老年代区域集合
如果 G1 预测在完成最小集合回收后仍然有剩余时间，则会从候选区域中继续加入老年代区域，直到预测使用约 80% 的剩余停顿时间为止。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可选收集集合（Optional collection set）
在完成前两部分之后，如果本次停顿仍然有时间，G1 会逐步疏散这部分可选区域。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;前两个集合会在一次初始收集过程中完成，而可选集合则会在剩余时间内逐步处理。这种方式既保证了空间回收的进度，又提高了停顿时间控制的成功率，同时降低了可选集合管理带来的额外开销。&lt;/p&gt;
&lt;p&gt;当收集集合候选区域不再有可用区域时，空间回收阶段结束。&lt;/p&gt;
&lt;p&gt;更多关于 G1 使用多少老年代区域以及如何避免较长 mixed 收集停顿的信息，请参见&lt;a class="link" href="https://docs.oracle.com/en/java/javase/21/gctuning/garbage-first-garbage-collector-tuning.html#GUID-90E30ACA-8040-432E-B3A0-1E0440AB556A" target="_blank" rel="noopener"
 &gt;《Garbage-First 垃圾收集器调优》&lt;/a&gt;。&lt;/p&gt;
&lt;h3 id="周期性垃圾回收periodic-garbage-collections"&gt;周期性垃圾回收（Periodic Garbage Collections）
&lt;/h3&gt;&lt;p&gt;如果由于应用程序处于空闲状态而长时间没有发生垃圾回收，虚拟机可能会长时间持有大量未使用的内存，而这些内存本可以被其他用途使用。为了避免这种情况，G1 可以通过 &lt;code&gt;-XX:G1PeriodicGCInterval&lt;/code&gt; 参数强制定期执行垃圾回收。&lt;/p&gt;
&lt;p&gt;该参数用于指定 G1 进行垃圾回收的最小时间间隔（单位为毫秒）。如果自上一次垃圾回收停顿以来已经超过该时间，且当前没有并发回收周期正在进行，则 G1 会触发额外的垃圾回收，其可能行为如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在仅年轻代阶段（Young-Only phase）期间：
G1 会通过一次 Concurrent Start 停顿启动并发标记（concurrent marking）；如果指定了 &lt;code&gt;-XX:-G1PeriodicGCInvokesConcurrent&lt;/code&gt;，则会触发一次 Full GC。&lt;/li&gt;
&lt;li&gt;在空间回收阶段（Space Reclamation phase）期间：
G1 会继续空间回收阶段，并触发与当前进度相匹配的垃圾回收停顿类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;-XX:G1PeriodicGCSystemLoadThreshold&lt;/code&gt; 参数可以用于进一步限制是否触发周期性垃圾回收：如果 JVM 所在系统（例如容器）通过 &lt;code&gt;getloadavg()&lt;/code&gt; 返回的一分钟平均系统负载高于该阈值，则不会执行周期性垃圾回收。&lt;/p&gt;
&lt;p&gt;更多关于周期性垃圾回收的信息，请参见&lt;a class="link" href="https://openjdk.org/jeps/346" target="_blank" rel="noopener"
 &gt;JEP 346：《Promptly Return Unused Committed Memory from G1》&lt;/a&gt;。&lt;/p&gt;
&lt;h3 id="确定初始堆占用率determining-initiating-heap-occupancy"&gt;确定初始堆占用率（Determining Initiating Heap Occupancy）
&lt;/h3&gt;&lt;p&gt;初始堆占用率百分比（Initiating Heap Occupancy Percent，IHOP）是触发一次 Concurrent Start 收集的阈值，它被定义为老年代大小的一个百分比。&lt;/p&gt;
&lt;p&gt;G1 默认会通过观察标记（marking）所花费的时间，以及在标记周期中老年代通常的内存分配情况，自动计算一个最优的 IHOP。这一特性称为 &lt;strong&gt;自适应 IHOP（Adaptive IHOP）&lt;/strong&gt;。如果该功能处于启用状态，那么 &lt;code&gt;-XX:InitiatingHeapOccupancyPercent&lt;/code&gt; 选项仅在系统还没有足够观测数据以准确预测 IHOP 阈值时，作为初始值使用，其含义是当前老年代大小的一个百分比。可以通过 &lt;code&gt;-XX:-G1UseAdaptiveIHOP&lt;/code&gt; 关闭这一行为。在这种情况下，&lt;code&gt;-XX:InitiatingHeapOccupancyPercent&lt;/code&gt; 的值将始终决定该触发阈值。&lt;/p&gt;
&lt;p&gt;在内部实现中，自适应 IHOP 的目标是将“初始堆占用率”设置为：当老年代占用达到一个值时，空间回收阶段的第一次 mixed 垃圾回收刚好开始。这个目标值通常等于当前最大老年代容量减去 &lt;code&gt;-XX:G1HeapReservePercent&lt;/code&gt; 所预留的额外缓冲空间。&lt;/p&gt;
&lt;h3 id="标记marking"&gt;标记（Marking）
&lt;/h3&gt;&lt;p&gt;G1 的标记使用一种称为 &lt;strong&gt;“起始快照（Snapshot-At-The-Beginning，SATB）”&lt;/strong&gt; 的算法。它会在初始标记（Initial Mark）停顿时对堆创建一个虚拟快照，在标记开始时处于存活状态的所有对象，在整个标记过程中都被认为是存活的。这意味着，在标记过程中变为死亡（不可达）的对象，在空间回收时仍然会被当作存活对象处理（某些情况除外）。与其他收集器相比，这可能会导致一些额外的内存被“错误保留”。然而，SATB 可以在一定程度上提供更好的延迟表现，尤其是在 Remark 停顿期间。那些在标记期间被过于保守地认为存活的对象，会在下一次标记过程中被回收。&lt;/p&gt;
&lt;p&gt;关于标记相关问题的更多信息，请参见&lt;a class="link" href="https://docs.oracle.com/en/java/javase/21/gctuning/garbage-first-garbage-collector-tuning.html#GUID-90E30ACA-8040-432E-B3A0-1E0440AB556A" target="_blank" rel="noopener"
 &gt;《Garbage-First 垃圾收集器调优》&lt;/a&gt;。&lt;/p&gt;
&lt;h3 id="极度紧张堆空间情况下的行为behavior-in-very-tight-heap-situations"&gt;极度紧张堆空间情况下的行为（Behavior in Very Tight Heap Situations）
&lt;/h3&gt;&lt;p&gt;当应用程序长期保持大量对象存活，以至于疏散（evacuation）无法找到足够空间进行对象复制时，就会发生 &lt;strong&gt;疏散失败（Evacuation Failure）&lt;/strong&gt;。疏散失败意味着 G1 会尝试完成当前垃圾收集：已经成功移动的对象保持在新位置，而尚未移动的对象不会再被复制，只是对对象之间的引用进行调整。疏散失败可能会带来一定额外开销，但通常其速度仍接近一次普通的年轻代收集。在发生该失败的垃圾收集完成后，G1 会正常恢复应用程序执行，而不会立即采取其他措施。G1 会假设疏散失败发生在垃圾收集接近尾声时，即大部分对象已经被移动，并且剩余空间足够支撑应用继续运行，直到标记完成并进入空间回收阶段。&lt;/p&gt;
&lt;p&gt;如果这一假设不成立，那么 G1 最终会触发一次 Full GC。这种收集会对整个堆进行原地压缩，可能非常缓慢。&lt;/p&gt;
&lt;p&gt;关于分配失败或 Full GC 早于 OOM 发生的问题，请参见&lt;a class="link" href="https://docs.oracle.com/en/java/javase/21/gctuning/garbage-first-garbage-collector-tuning.html#GUID-90E30ACA-8040-432E-B3A0-1E0440AB556A" target="_blank" rel="noopener"
 &gt;《Garbage-First 垃圾收集器调优》&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;巨型对象（Humongous Objects）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;巨型对象是指大小大于或等于一个 Region（区域）一半的对象。当前 Region 的大小会根据 G1 的“自动调优默认值（Ergonomic Defaults for G1 GC）”来确定，除非通过 &lt;code&gt;-XX:G1HeapRegionSize&lt;/code&gt; 参数显式设置。&lt;/p&gt;
&lt;p&gt;这些巨型对象在某些情况下会被特殊处理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;分配方式&lt;/strong&gt;
每个巨型对象都会被分配到老年代中一段连续的 Region 序列中。对象本身的起始位置总是位于该序列中第一个 Region 的开头。该序列中最后一个 Region 可能存在剩余空间，但这部分空间在整个对象被回收之前无法用于其他分配。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;回收方式&lt;/strong&gt;
通常情况下，巨型对象只能在标记结束后的 Cleanup 停顿中被回收，或者在 Full GC 中如果它们已不可达时被回收。
但对于某些特殊情况（例如 primitive 类型数组，如 bool、各种整数类型以及浮点数数组），G1 有一个特殊优化：在任何垃圾回收停顿中，如果这些巨型对象没有被大量对象引用，G1 会尝试主动回收它们。该行为默认开启，可以通过 &lt;code&gt;-XX:G1EagerReclaimHumongousObjects&lt;/code&gt; 关闭。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;分配对 GC 的影响&lt;/strong&gt;
巨型对象的分配可能会导致垃圾回收提前发生。G1 在每次巨型对象分配时都会检查 IHOP（Initiating Heap Occupancy）阈值，如果当前堆占用超过该阈值，则可能立即触发一次 Initial Mark young collection。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;移动行为（非常重要）&lt;/strong&gt;
巨型对象只有在极端情况下才会被移动：
当第一次 Full GC 仍无法为巨型对象分配足够的连续空间时，G1 会在同一次停顿中触发第二次 Full GC 进行“最后手段”的回收尝试。这个过程非常慢。由于包含巨型对象尾部的 Region 可能存在不可用空间，即使经过这些处理，仍然可能导致 JVM 因为内存不足而退出（OutOfMemoryError）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="g1-gc-的自适应默认值ergonomic-defaults-for-g1-gc"&gt;G1 GC 的自适应默认值（Ergonomic Defaults for G1 GC）
&lt;/h2&gt;&lt;p&gt;本主题概述了 G1 中最重要的一些默认配置及其默认取值。这些默认值在不使用任何额外参数的情况下，提供了对 G1 行为和资源使用情况的基本预期。&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;选项与默认值&lt;/th&gt;
 &lt;th style="text-align: left"&gt;说明&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;-XX:MaxGCPauseMillis=200&lt;/code&gt;&lt;/td&gt;
 &lt;td style="text-align: left"&gt;最大停顿时间目标。&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;-XX:GCPauseTimeInterval=&amp;lt;ergo&amp;gt;&lt;/code&gt;&lt;/td&gt;
 &lt;td style="text-align: left"&gt;最大停顿时间间隔目标。默认情况下 G1 不设置该目标，允许在极端情况下连续执行垃圾回收（back-to-back）。&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;-XX:ParallelGCThreads=&amp;lt;ergo&amp;gt;&lt;/code&gt;&lt;/td&gt;
 &lt;td style="text-align: left"&gt;垃圾回收停顿期间用于并行工作的最大线程数。该值由 JVM 运行所在机器的可用线程数推导而来：如果可用 CPU 线程数小于等于 8，则直接使用该值；否则使用“超过 8 的线程数的五分之八”加上 8 作为最终线程数。在每次停顿开始时，该线程数还会受到最大堆大小限制：G1 不会为每 &lt;code&gt;-XX:HeapSizePerGCThread&lt;/code&gt; 大小的 Java 堆容量分配超过一个线程。&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;-XX:ConcGCThreads=&amp;lt;ergo&amp;gt;&lt;/code&gt;&lt;/td&gt;
 &lt;td style="text-align: left"&gt;并发标记阶段使用的最大线程数。默认值为 &lt;code&gt;-XX:ParallelGCThreads / 4&lt;/code&gt;。&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;-XX:+G1UseAdaptiveIHOP&lt;/code&gt; &lt;code&gt;-XX:InitiatingHeapOccupancyPercent=45&lt;/code&gt;&lt;/td&gt;
 &lt;td style="text-align: left"&gt;控制初始堆占用率（IHOP）的默认设置：启用自适应 IHOP；在最初几个收集周期中，G1 会使用老年代 45% 的占用率作为标记起始阈值。&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;-XX:G1HeapRegionSize=&amp;lt;ergo&amp;gt;&lt;/code&gt;&lt;/td&gt;
 &lt;td style="text-align: left"&gt;堆区域（Region）的大小。默认值基于最大堆大小计算，目标是生成约 2048 个区域，最大值经过调优不超过 32 MB。用户指定值必须是 2 的幂，合法范围为 1 MB 到 512 MB。&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;-XX:G1NewSizePercent=5&lt;/code&gt; &lt;code&gt;-XX:G1MaxNewSizePercent=60&lt;/code&gt;&lt;/td&gt;
 &lt;td style="text-align: left"&gt;年轻代大小的范围，占当前 Java 堆的百分比，在这两个值之间动态变化。&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;-XX:G1HeapWastePercent=5&lt;/code&gt;&lt;/td&gt;
 &lt;td style="text-align: left"&gt;收集集合候选区域中允许未回收空间的比例（百分比）。当候选区域中的可回收空间低于该值时，G1 会停止空间回收阶段。&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;-XX:G1MixedGCCountTarget=8&lt;/code&gt;&lt;/td&gt;
 &lt;td style="text-align: left"&gt;空间回收阶段的期望长度，以垃圾收集次数（collections）为单位。&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;-XX:G1MixedGCLiveThresholdPercent=85&lt;/code&gt;&lt;/td&gt;
 &lt;td style="text-align: left"&gt;老年代区域中存活对象比例超过该百分比的区域，不会在本次空间回收阶段被收集。&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote class="alert alert-note"&gt;
 &lt;div class="alert-header"&gt;
 &lt;span class="alert-icon"&gt;📝&lt;/span&gt;
 &lt;span class="alert-title"&gt;备注&lt;/span&gt;
 &lt;/div&gt;
 &lt;div class="alert-body"&gt;
 &lt;p&gt;&lt;code&gt;&amp;lt;ergo&amp;gt;&lt;/code&gt; 表示该实际取值是根据运行环境通过“自适应调优（ergonomics）”自动决定的。&lt;/p&gt;
 &lt;/div&gt;
 &lt;/blockquote&gt;
&lt;h2 id="与其他收集器的比较comparison-to-other-collectors"&gt;与其他收集器的比较（Comparison to Other Collectors）
&lt;/h2&gt;&lt;p&gt;这是 G1 与其他垃圾收集器之间主要差异的总结：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;**Parallel GC（并行收集器）**只能在一次整体操作中压缩并回收老年代空间。相比之下，G1 将这项工作分摊到多次更短的收集过程中逐步完成。这显著缩短了停顿时间，但可能会以吞吐量下降为代价。&lt;/li&gt;
&lt;li&gt;G1 会在并发过程中执行部分老年代空间回收工作。&lt;/li&gt;
&lt;li&gt;由于其并发特性，G1 可能比上述收集器具有更高的开销，从而影响整体吞吐量。&lt;/li&gt;
&lt;li&gt;ZGC 的目标是提供更小的停顿时间，但代价是进一步降低吞吐量。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由于其工作方式，G1 具有一些机制来提升垃圾回收效率：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;G1 可以在任意一次垃圾回收中回收老年代中一些完全空的大区域。这可以避免许多不必要的垃圾回收，在较低开销下释放大量空间。&lt;/li&gt;
&lt;li&gt;G1 可以选择性地在 Java 堆中并发去重重复字符串。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;回收老年代中完全空的大对象（humongous objects）的机制始终是启用的。可以通过 &lt;code&gt;-XX:-G1EagerReclaimHumongousObjects&lt;/code&gt; 禁用该功能。&lt;/p&gt;
&lt;p&gt;字符串去重默认是关闭的，可以通过 &lt;code&gt;-XX:+UseStringDeduplication&lt;/code&gt; 启用。&lt;/p&gt;</description></item></channel></rss>