G1、ZGC有了解过吗?
两者的发布时间线大致如下:
- G1 (Garbage-First)
- 初体验:随 JDK 6 Update 14 作为体验版发布。
- 正式推出:在 JDK 7 Update 4 中被正式推出,标志着它成为一个可用的生产环境收集器。
- 成为默认:自 JDK 9 起,G1 被设置为默认的垃圾回收器,取代了之前的 Parallel GC。
- ZGC (Z Garbage Collector)
- 实验性质:在 JDK 11 中作为实验性功能首次引入。
- 生产就绪:在 JDK 15 中被正式宣布为生产就绪,可以放心在生产环境使用。
- 重要演进:在 JDK 21 中进行了重大升级,引入了分代ZGC (Generational ZGC),进一步优化了性能和内存开销。
核心特性整理:
| 特性 | G1 (Garbage-First) | ZGC (Z Garbage Collector) |
|---|---|---|
| 设计目标 | 在可控的停顿时间下,获得尽可能高的吞吐量。 | 实现极致低延迟,将GC停顿时间控制在亚毫秒级,且与堆大小无关。 |
| 核心技术 | Region化分代:将堆划分为多个大小相等的Region,动态扮演Eden、Survivor、Old等角色。 SATB写屏障:用于并发标记,减少最终标记阶段的停顿。 | 着色指针 (Colored Pointers):在指针中编码GC状态信息。 读屏障 (Load Barriers):在应用线程读取对象引用时执行,实现“指针自愈”。 |
| 吞吐量影响 | 相对较低,对应用吞吐量影响较小,适合大多数通用场景。 | 会带来额外的CPU开销,官方表示吞吐量下降通常不超过15%。 |
| 适用场景 | 堆内存较大(4GB~64GB),追求平衡吞吐量和延迟的通用企业级应用,是默认推荐选项。 | 堆内存极大(可达16TB),对响应时间有极高要求的场景,如金融交易、实时风控等。 |
# G1核心设计是怎么样的?
G1核心设计是基于 Region 的堆内存布局。
G1 最根本的变革,是放弃了传统 GC(如 Parallel GC)中新生代、老年代物理连续的划分方式,而是将整个堆内存划分为大小相等的 Region(区域)。
- 每个 Region 的大小通常在 1MB 到 32MB 之间,具体由 JVM 在启动时根据堆大小自动计算。
- 每个 Region 在逻辑上可以扮演 Eden、Survivor、老年代(Old Gen) 甚至 大对象区(Humongous) 的角色。这个角色是动态的,在一次 GC 后,一个 Region 可以从 Eden 变为 Survivor,或者从 Old 变为空闲区(Free)。
这种设计的最大优势是灵活性。G1 不需要一次回收整个新生代或老年代,而是可以选择任意多个 Region 构成一个回收集(Collection Set, CSet),回收其中的垃圾。这正是“Garbage-First”(垃圾优先)名字的由来——它总是优先回收垃圾占比最高的 Region。
# G1 的优缺点
| 优点 | 缺点 |
|---|---|
| 可预测的停顿时间:通过停顿预测模型,G1 能将 GC 停顿控制在用户指定的范围内。 | 内存开销略高:由于需要维护 Region 元数据、RSet(Remembered Set)等,G1 的内存占用比 Parallel GC 稍高。 |
| 高吞吐量:在大多数场景下,G1 的吞吐量表现优异。 | 配置较复杂:虽然默认参数已经适用大多数场景,但要调优到最佳状态,需要深入理解其机制。 |
| 自动调优:G1 会根据运行时动态调整新生代大小、晋升阈值等参数,减轻了人工调优负担。 | 大对象分配敏感:频繁分配大对象可能导致 Full GC,需要额外关注。 |
| 适合大堆:在 4GB~64GB 的堆内存上,G1 表现尤为出色。 |
常用 JVM 参数:
| 参数 | 说明 | 默认值 |
|---|---|---|
-XX:+UseG1GC | 启用 G1 GC(JDK 9+ 默认开启) | |
-XX:MaxGCPauseMillis | 设定期望的最大停顿时间,单位毫秒。G1 会尽力达到这个目标,但不是硬性保证。 | 200ms |
-XX:G1HeapRegionSize | 设置每个 Region 的大小(必须为 2 的幂次,范围 1MB~32MB)。 | 由 JVM 根据堆大小自动计算 |
-XX:InitiatingHeapOccupancyPercent | 触发并发标记周期的堆占用百分比(相对于整个堆)。 | 45% |
-XX:G1ReservePercent | 预留空闲 Region 的百分比,用于“晋升”失败时的兜底。 | 10% |
-XX:ConcGCThreads | 并发标记阶段的线程数。 | 根据 CPU 核心数自动计算 |
-XX:ParallelGCThreads | STW 阶段并行执行的线程数。 | 根据 CPU 核心数自动计算 |
# 为什么ZGC的 STW 这么快?
ZGC的STW(Stop-The-World)之所以能达到毫秒乃至微秒级别,关键在于它从设计哲学到核心算法都进行了颠覆式的创新。它将几乎所有耗时的垃圾回收工作都变为与应用线程并发执行,而STW的暂停时间与堆的大小、存活对象的数量几乎没有关系。
它之所以这么快,主要归功于两项革命性的技术:
- 着色指针 (Colored Pointers):ZGC颠覆了传统,不在对象头里记录GC信息,而是巧妙地利用64位指针中未被使用的位来存储标记和重定位状态。这让ZGC能在不访问内存的情况下快速判断对象状态,实现了“指针自愈”。
- 读屏障 (Load Barriers):JVM在应用程序每次从堆中读取对象引用时,都会插入一小段“读屏障”代码。当应用线程通过“着色指针”访问对象时,读屏障会检查指针颜色。如果发现对象正被移动(颜色“坏了”),它会立即“治愈”这个引用,将其指向正确的新地址,确保应用线程永远访问到有效数据。
基于这两项技术,ZGC的整个GC周期(并发标记、并发转移等)几乎全程与应用并发执行,仅有初始标记、最终标记、初始转移这几个极短的阶段需要STW,并且这些阶段的耗时仅与GC Roots的数量相关,和堆的大小无关。
# 那么,既然ZGC这么好,是所有场景的首选吗?
答案是否定的。 任何技术选择都有其适用边界和代价,ZGC也不例外。
ZGC的核心优势是极致的低延迟,但它并非没有短板:
- 额外的CPU开销:因为读屏障在每次对象引用加载时都会执行,这会消耗一定的CPU资源。虽然官方测试表明整体吞吐量下降通常不超过15%,但在CPU资源本身就非常紧张的系统里,这个开销需要评估。
- 需要更大的堆内存(在某些情况下):在某些高分配速率的场景下,为了达到其设计的吞吐量目标,ZGC可能需要比G1更大的堆内存作为“喘息空间”。
- 内存占用显示异常:由于ZGC的多重映射技术,一些监控工具(如top)可能会误报其内存占用为实际值的数倍,给运维带来困扰。
因此,选择ZGC的核心原则是:是否愿意牺牲一点CPU利用率和可能的内存成本,来换取确定性极强的超低GC停顿。
# 如何选择垃圾回收器?
G1 是一个平衡型的垃圾回收器:
- 它通过 Region 化内存布局 和 停顿预测模型,实现了可预测的低延迟。
- 它通过 并发标记 和 增量回收,将大堆上的 GC 停顿控制在可接受范围内。
- 它通过 自动调优,减轻了开发者的配置负担。
如果你的应用堆内存较大(>4GB),且对延迟有一定要求(不是极致的毫秒级,而是百毫秒级),G1 是一个非常稳妥的默认选择。如果你的应用对延迟有极致的亚毫秒级要求,或者堆内存超过 64GB,那么可以进一步考虑 ZGC。
ZGC STW 时间很短,但:
- ZGC是“低延迟”场景下的利器,但它不是“银弹”。选择它需要你明确知道自己的应用是否能接受那15%以内的吞吐量开销,以及是否做好了适配和观察的准备。
- 如果你的应用对响应时间非常敏感,常常因为GC停顿而影响用户体验或产生超时,那么ZGC是你的首选。在JDK 17及更高版本,通过
-XX:+UseZGC参数即可启用,而**从JDK 21开始,Generational ZGC(分代ZGC)**进一步优化了性能和内存开销,是更具吸引力的选择。 - 如果应用对延迟不敏感,更看重CPU资源的充分利用,那么G1甚至Parallel GC可能是更经济的选择。
对比:
| 回收器 | 核心目标 | 典型STW停顿 | 适用场景 |
|---|---|---|---|
| ZGC | 极致低延迟 | < 1ms,且与堆大小无关 | 对响应时间有极高要求:金融交易、实时风控、在线游戏、大型互联网服务 |
| G1 | 平衡吞吐量与延迟 | 通常几十到几百毫秒 | 通用场景:大多数微服务、Web应用,是GC领域的“万金油” |
| Parallel GC | 最大化吞吐量 | 暂停时间可能较长(秒级) | 批处理、后台计算:对响应时间不敏感,更关注任务处理速度的任务 |