JVM GC 深度破局:G1 与 ZGC 底层原理、生产调优全链路实战

前言

在Java生态中,垃圾收集器(GC)是决定系统性能、吞吐量与响应延迟的核心组件。生产环境中90%以上的JVM性能瓶颈、服务卡顿、OOM故障,本质都与GC的不合理配置与运行机制相关。JDK17作为当前企业级应用的主流LTS版本,G1是默认的服务端收集器,ZGC则是革命性的低延迟收集器,二者覆盖了绝大多数业务场景的GC需求。

一、GC核心前置知识(必知)

所有垃圾收集器的设计都基于核心的GC基础理论,理解这些内容是拆解G1与ZGC底层逻辑的前提,本文仅聚焦与两款收集器强相关的核心知识点。

1.1 垃圾判定核心:可达性分析算法

JVM通过可达性分析判定对象是否为垃圾,核心逻辑是:以GC Roots为根节点,遍历所有对象引用链,无法被GC Roots直接或间接引用的对象,即为可回收的垃圾。

GC Roots的固定范围(权威来源:JVM规范):

  • 虚拟机栈中本地变量表引用的对象

  • 本地方法栈中JNI(Native方法)引用的对象

  • 方法区中类静态属性引用的常量对象

  • 方法区中常量引用的对象

  • 同步锁(synchronized)持有的对象

  • JVM内部的基础类对象、系统类加载器等

1.2 并发标记核心:三色标记算法

并发标记是实现GC与用户线程并行运行的核心,所有现代垃圾收集器均基于三色标记算法实现,核心是将对象分为三类:

  • 黑色对象:已被GC扫描过,自身与所有直接引用的对象都完成标记,不会被再次扫描

  • 灰色对象:已被GC扫描过,但自身还有未被扫描的引用对象,是标记过程的中间状态

  • 白色对象:未被GC扫描过,标记完成后仍为白色的对象即为垃圾,会被回收

并发标记的致命问题是对象漏标,即存活的对象被标记为白色垃圾,导致程序崩溃。漏标必须同时满足两个条件:

  1. 黑色对象新增了对白色对象的直接引用

  2. 所有灰色对象删除了对该白色对象的直接引用

针对漏标问题,行业有两种成熟的解决方案,也是G1与CMS的核心区别:

  • 增量更新(Incremental Update):破坏第一个条件,黑色对象新增引用时,将其变为灰色,重新扫描,CMS收集器采用该方案

  • SATB(Snapshot At The Beginning,起始快照):破坏第二个条件,灰色对象删除引用时,将该引用记录下来,标记结束后重新扫描,G1收集器采用该方案

1.3 核心概念定义

  • STW(Stop The World):暂停所有用户线程,仅执行GC线程,是影响系统延迟的核心因素,所有GC调优的核心目标都是降低STW的时长与频率

  • 分代回收假说 :现代分代收集器的设计基础,包含三条核心定律

    1. 弱分代假说:绝大多数对象都是朝生夕灭的

    2. 强分代假说:熬过越多次GC的对象,越难被回收

    3. 跨代引用假说:跨代引用相对于同代引用仅占极少数

  • 内存屏障:GC拦截用户线程的引用操作,实现并发标记与并发转移的核心机制,分为写屏障(拦截引用修改)与读屏障(拦截引用读取)

二、G1垃圾收集器深度剖析

G1(Garbage-First)是JDK9及以上版本的默认服务端垃圾收集器,JDK17中已完全成熟稳定,设计目标是兼顾吞吐量与可控的延迟,支持用户设置最大停顿时间目标,适用堆内存4GB~64GB的业务场景,是企业级应用的首选默认收集器。

2.1 G1核心内存布局:Region化分代设计

G1彻底打破了CMS等传统收集器的连续分代内存布局,将整个Java堆划分为多个大小相等、物理上不连续的独立Region,每个Region都可以根据需要,动态扮演Eden区、Survivor区、Old区、Humongous区,实现了内存的弹性管理。

核心规则与参数定义(JDK17权威规范)
  1. Region大小规则 :必须是2的幂,取值范围为1MB~32MB,JVM自动计算规则为堆内存大小/2048,最终向上取最近的2的幂值。例如8GB堆内存,自动计算的Region大小为4MB。

  2. Eden Region:年轻代,存放新创建的对象,默认初始占比5%,最大不超过堆内存的60%,G1会根据停顿时间目标动态调整Eden区的大小。

  3. Survivor Region:年轻代,分为From和To两个区域,存放Eden GC后存活的对象,默认占比不超过年轻代的10%。

  4. Old Region:老年代,存放熬过多次Young GC后晋升的对象,是并发标记与混合GC的核心回收目标。

  5. Humongous Region:大对象专属区域,存放大小超过Region 50%的对象,直接分配在老年代,不会进入年轻代。JDK17对大对象回收做了优化,在Young GC阶段即可回收无引用的大对象,避免了老年代内存的无效占用。

2.2 G1完整回收流程全解

G1的回收流程分为四个核心阶段,每个阶段的运行逻辑、STW时长都有明确的规则,完整流程如下:

阶段1:年轻代GC(Young GC)
  • 触发条件:Eden Region被占满时触发

  • 运行逻辑:STW全停顿,将Eden和Survivor中存活的对象复制到新的Survivor Region,符合晋升年龄阈值的对象直接晋升到老年代Region,回收所有空闲的Eden Region

  • 核心特性 :G1会根据用户设置的MaxGCPauseMillis最大停顿时间目标,动态调整Eden Region的数量,实现可预测的停顿时间,这是G1相比CMS的核心优势

  • STW时长:与Eden Region的数量成正比,默认控制在200ms以内

阶段2:并发标记周期(Concurrent Marking Cycle)
  • 触发条件 :老年代占用率达到IHOP(InitiatingHeapOccupancyPercent)阈值时触发,JDK17默认开启自适应IHOP,会根据GC历史自动调整阈值,默认初始值为45%

  • 核心阶段拆解

    1. 初始标记(Initial Mark):STW停顿,标记GC Roots直接可达的对象,该阶段附着在Young GC上执行,不会产生额外的STW开销

    2. 根区域扫描(Root Region Scan):并发执行,扫描Survivor Region中引用的老年代对象,完成标记,该阶段必须在下次Young GC之前完成

    3. 并发标记(Concurrent Mark):并发执行,与用户线程并行运行,遍历整个堆内存,标记所有可达对象,期间如果发现Region内所有对象都是垃圾,会立即回收该Region

    4. 重新标记(Remark):STW停顿,处理SATB写屏障记录的引用变化,完成最终的存活对象标记,得益于SATB机制,该阶段的STW时长远低于CMS的重新标记阶段

    5. 清理阶段(Cleanup):部分STW,统计每个Region的存活对象占比,按照回收收益(释放内存大小/复制耗时)排序,回收完全空闲的Region,确定混合GC的回收目标Region集合

阶段3:混合GC(Mixed GC)
  • 触发条件:并发标记周期完成后触发

  • 运行逻辑:STW全停顿,不仅回收年轻代的所有Region,还会回收并发标记阶段筛选出的、回收收益高的老年代Region,将存活对象复制到新的空闲Region,释放原Region的内存

  • 核心特性:G1会根据最大停顿时间目标,拆分老年代Region的回收次数,默认分8次完成所有目标Region的回收,避免单次STW时长过长

  • 终止条件:老年代内存占用率降至阈值以下,回到Young GC阶段

阶段4:Full GC
  • 触发条件:堆内存不足,混合GC的回收速度跟不上对象分配速度、巨型对象无法找到连续的Region分配、元空间不足等场景触发

  • 运行逻辑:JDK10及以上版本为多线程执行,全程STW全停顿,采用标记-整理算法对整个堆内存做全量回收与压缩

  • 调优目标:生产环境必须尽量避免Full GC,其STW时长通常在秒级,会导致系统严重卡顿、服务不可用

2.3 G1核心算法:SATB与写屏障

G1通过SATB(起始快照)+ 写屏障解决并发标记的漏标问题,这是G1的核心算法,也是其相比CMS的核心优势。

SATB的核心逻辑是:并发标记开始时,对当前堆内存的对象引用关系拍一个快照,所有在快照中存活的对象,都被视为本次标记周期内的存活对象,即使后续被删除引用,也不会被标记为垃圾。

当用户线程修改对象引用时,G1的写屏障会拦截该操作:如果是灰色对象删除了对白色对象的引用,写屏障会将该引用记录到SATB缓冲区,在重新标记阶段,GC会扫描所有SATB缓冲区的引用,确保这些对象不会被漏标,彻底破坏了漏标的第二个必要条件。

相比CMS的增量更新,SATB的优势在于:重新标记阶段不需要重新扫描整个堆,只需要扫描SATB缓冲区,大幅缩短了STW时长,同时降低了并发标记的CPU开销。

2.4 G1核心参数与生产调优实战

2.4.1 JDK17 G1核心有效参数
参数名 作用说明 默认值 调优建议
-Xms/-Xmx 堆内存初始值与最大值 生产环境必须设置为相同值,避免堆扩容开销
-XX:+UseG1GC 启用G1收集器 JDK9+默认开启 生产环境建议显式声明,明确收集器类型
-XX:MaxGCPauseMillis 最大停顿时间目标 200ms 不建议设置低于100ms,否则会导致Young GC频率飙升,吞吐量下降
-XX:InitiatingHeapOccupancyPercent 老年代占用触发并发标记阈值 45% 大对象较多的场景,建议下调至30%~40%,提前触发并发标记
-XX:+AdaptiveIHOP 开启自适应IHOP调整 默认开启 生产环境建议保持开启,JVM会根据GC历史自动调整阈值
-XX:G1HeapRegionSize 手动设置Region大小 自动计算 仅在大对象过多的场景手动设置,必须为2的幂,范围1MB~32MB
-XX:G1MixedGCCountTarget 混合GC拆分执行次数 8 单次混合GC停顿过长时,可上调至10~12,拆分回收压力
-XX:G1OldCSetRegionThresholdPercent 单次混合GC最大回收老年代Region占比 10% 不建议超过15%,避免单次STW时长超标
2.4.2 生产调优实战案例

场景 :8核16G服务器,JDK17微服务系统,堆内存设置10G,默认G1参数,业务高峰期出现频繁Full GC,系统RT从正常50ms飙升至500ms以上,服务频繁告警。 排查过程

  1. 分析GC日志,发现Full GC的触发原因是Humongous Allocation Failure,巨型对象分配失败

  2. 计算JVM自动设置的Region大小为4MB,巨型对象阈值为2MB,业务高峰期大量2MB~4MB的临时对象被判定为巨型对象,占用老年代内存

  3. 老年代内存占用率快速飙升至90%以上,并发标记周期尚未完成,导致对象分配失败,触发Full GC 调优方案

  4. 手动设置Region大小为8MB,-XX:G1HeapRegionSize=8M,将巨型对象阈值提升至4MB,减少巨型对象数量

  5. 调整自适应IHOP的最低阈值,-XX:G1AdaptiveIHOPMinInitiatingOccupancy=30,提前触发并发标记

  6. 调整最大停顿时间目标为250ms,-XX:MaxGCPauseMillis=250,给GC预留更多执行时间,降低Young GC频率

  7. 调整混合GC拆分次数为10,-XX:G1MixedGCCountTarget=10,拆分老年代回收压力,避免单次STW过长 调优结果:高峰期Full GC完全消失,Young GC平均停顿时间稳定在150ms以内,混合GC平均停顿时间控制在200ms以内,系统RT稳定在50ms以内,吞吐量提升32%。

三、ZGC垃圾收集器深度剖析

ZGC是Oracle在JDK11中引入的低延迟垃圾收集器,JDK15正式生产就绪,JDK17 LTS版本中已完全稳定,设计目标是在堆内存从MB级到TB级的场景下,STW停顿时间不超过1ms,同时吞吐量损失不超过15% 。相比G1,ZGC将GC停顿降低了两个数量级,是当前JVM生态中最先进的低延迟收集器。

3.1 ZGC核心设计理念

ZGC是一款基于Region的、单代(JDK17默认)、全并发、基于标记-复制算法的垃圾收集器,核心突破是:几乎所有GC阶段都与用户线程并发执行,STW停顿仅与GC Roots的数量相关,与堆内存大小完全无关,哪怕是10TB的堆内存,STW停顿也能稳定控制在1ms以内,彻底解决了大堆场景下的GC停顿问题。

注意:JDK17中的分代ZGC为预览功能,需要添加--enable-preview参数启用,生产环境不建议使用,分代ZGC在JDK21中才正式GA。

3.2 ZGC核心内存布局与黑科技

3.2.1 内存布局:动态ZPage设计

ZGC同样采用Region化内存布局,其Region称为ZPage,与G1固定大小的Region不同,ZPage的大小是动态的,分为三类:

  • 小型ZPage:固定2MB,存放小于256KB的对象

  • 中型ZPage:固定32MB,存放256KB~4MB的对象

  • 大型ZPage:2MB的整数倍,存放大于4MB的对象,每个大型ZPage仅存放一个对象,不会被压缩复制

3.2.2 核心黑科技1:染色指针(Colored Pointers)

染色指针是ZGC实现全并发的核心,也是其区别于所有其他收集器的革命性设计。

ZGC仅支持64位系统,64位虚拟地址的寻址空间极大,但实际操作系统仅使用了低42位(最大支持4TB堆内存),ZGC将64位指针的高4位提取出来,作为对象的状态标记位,直接记录在引用指针中,而非对象头中。

4个标记位的核心作用:

  • M0/M1:两个交替的标记位,用于并发标记阶段标记存活对象,每次GC周期交替使用,避免清空所有标记位的开销

  • Remapped:重定位标记位,标记指针是否指向对象转移后的最新地址

  • Finalizable:可终结标记位,标记对象是否实现了finalize()方法

染色指针的核心优势:对象的状态信息直接存储在引用指针中,当对象被复制转移后,不需要立即修改所有引用该对象的指针,仅需通过读屏障在指针被访问时更新,实现了完全并发的对象转移。

3.2.3 核心黑科技2:多重映射(Multi-Mapping)

由于染色指针使用了高4位作为标记位,同一个物理内存地址,会对应多个不同的虚拟地址(标记位不同)。ZGC通过多重映射机制,将同一个物理内存页,映射到M0、M1、Remapped三个虚拟地址空间,无论指针的染色标记位是什么,都能通过虚拟地址正确访问到对应的物理内存,完美解决了染色指针的寻址问题。

3.3 ZGC完整回收流程全解

ZGC的完整回收周期分为7个核心阶段,其中仅3个阶段为STW停顿,且时长均不超过1ms,其余所有阶段均与用户线程并发执行,完整流程如下:

阶段1:初始标记(Mark Start)
  • 执行模式:STW全停顿

  • 核心逻辑:标记GC Roots直接可达的对象

  • STW时长:仅与GC Roots的数量相关,与堆大小无关,通常为几十微秒到几百微秒,不超过1ms

阶段2:并发标记(Concurrent Mark)
  • 执行模式:并发执行,与用户线程并行运行

  • 核心逻辑:遍历所有对象,标记所有可达的存活对象,通过读屏障拦截用户线程的引用读取操作,处理并发的引用变化,避免对象漏标

阶段3:最终标记(Mark End)
  • 执行模式:STW全停顿

  • 核心逻辑:处理标记阶段的剩余引用变化,完成最终的存活对象标记,统计所有需要回收的Region

  • STW时长:通常不超过1ms,与堆大小无关

阶段4:并发准备重定位(Concurrent Prepare for Relocate)
  • 执行模式:并发执行

  • 核心逻辑:统计所有垃圾占比高的Region,组成重定位集,计算需要转移的存活对象数量,规划重定位后的内存布局

阶段5:初始重定位(Relocate Start)
  • 执行模式:STW全停顿

  • 核心逻辑:转移GC Roots直接引用的对象,更新这些引用的指针地址与染色位

  • STW时长:仅与GC Roots的数量相关,通常不超过1ms,与堆大小无关

阶段6:并发重定位(Concurrent Relocate)
  • 执行模式:并发执行,与用户线程并行运行

  • 核心逻辑:转移重定位集中所有存活的对象到新的ZPage中,用户线程访问正在转移的对象时,读屏障会自动处理,确保访问到正确的对象地址,不会出现并发问题

阶段7:并发重映射(Concurrent Remap)
  • 执行模式:并发执行

  • 核心逻辑:更新所有指向转移后对象的引用指针,设置Remapped标记位,完成指针的"自愈"。该阶段不需要单独执行,ZGC会将其合并到下一次GC的并发标记阶段,避免额外的CPU开销,提升吞吐量

3.4 ZGC核心机制:读屏障

ZGC通过读屏障实现并发标记与并发重定位,这是与G1写屏障的核心区别,也是其实现全并发的关键。

读屏障的核心逻辑是:当用户线程读取对象引用时,读屏障会拦截该操作,检查指针的染色标记位:

  1. 如果对象已经被转移,读屏障会自动更新指针的地址与Remapped标记位,返回最新的对象地址,实现指针的"自愈"

  2. 如果对象正在被标记,读屏障会完成该对象的标记,避免漏标

与G1的写屏障对比,核心区别如下:

特性 G1收集器 ZGC收集器
屏障类型 写屏障 读屏障
触发时机 对象引用被修改时 对象引用被读取时
核心作用 记录引用删除,实现SATB,避免漏标 检查指针状态,实现并发转移,指针自愈
性能开销 写操作有额外开销,读操作无开销 读操作有轻微开销,写操作无开销
适用场景 写少读多,兼顾吞吐量与延迟 读多写少,极致低延迟需求

3.5 ZGC核心参数与生产调优实战

3.5.1 JDK17 ZGC核心有效参数
参数名 作用说明 默认值 调优建议
-Xms/-Xmx 堆内存初始值与最大值 生产环境必须设置为相同值,建议设置为峰值存活对象大小的2倍以上
-XX:+UseZGC 启用ZGC收集器 默认关闭 JDK17必须显式声明启用
-XX:MaxGCPauseMillis 最大停顿时间目标 1ms 生产环境可设置为1~2ms,预留更多执行空间
-XX:ConcGCThreads 并发GC线程数 CPU核心数的12.5% 建议设置为CPU核心数的1/4,提升并发GC处理速度
-XX:ParallelGCThreads STW阶段并行GC线程数 CPU核心数 建议设置为CPU核心数的1/2
-XX:+UseCompressedOops 开启压缩指针 默认开启 必须保持开启,ZGC依赖压缩指针实现染色指针
-XX:ZAllocationSpikeTolerance 分配突刺容忍系数 2.0 突发流量场景可上调至3.0~5.0,提前触发GC,避免分配停顿
3.5.2 生产调优实战案例

场景 :16核32G服务器,JDK17金融交易系统,要求P999响应时间不超过10ms,堆内存设置16G,启用ZGC默认参数,业务高峰期出现Allocation Stall(分配停顿),P999响应时间飙升至60ms以上,不符合交易系统要求。 排查过程

  1. 分析GC日志,发现并发GC的处理速度跟不上对象分配的速度,并发标记尚未完成,堆内存占用率已超过90%,用户线程必须等待GC完成才能分配对象,出现Allocation Stall

  2. 查看GC线程配置,默认并发GC线程数为2(16核的12.5%),高峰期GC线程CPU使用率持续100%,处理能力不足

  3. 堆内存预留缓冲空间不足,峰值存活对象大小为8G,16G堆仅预留了1倍的缓冲空间,无法应对突发的对象分配流量 调优方案

  4. 调整并发GC线程数为4,-XX:ConcGCThreads=4,提升并发GC的处理能力

  5. 调整堆内存为20G,给GC预留更多的缓冲空间,应对突发流量

  6. 调整最大停顿时间目标为2ms,-XX:MaxGCPauseMillis=2,给GC预留更多的执行时间

  7. 调整分配突刺容忍系数为3.0,-XX:ZAllocationSpikeTolerance=3.0,提前触发GC,避免分配停顿 调优结果:Allocation Stall完全消失,所有STW停顿均稳定在1ms以内,系统P999响应时间稳定在5ms以内,完全满足金融交易系统的低延迟要求。

四、G1与ZGC核心对比与选型指南

4.1 核心特性全对比(JDK17环境)

特性 G1收集器 ZGC收集器
设计目标 兼顾吞吐量与可控延迟 极致低延迟,吞吐量损失可控
分代设计 分代回收(年轻代+老年代) JDK17默认单代,分代为预览版
内存布局 固定大小Region(2的幂) 动态大小ZPage(小/中/大)
核心算法 SATB+写屏障,标记-复制+标记-整理 染色指针+读屏障,全并发标记-复制
STW停顿特性 最大停顿默认200ms,与堆大小正相关 停顿时间<1ms,与堆大小无关,仅与GC Roots数量相关
最大支持堆内存 最大约64GB 最大4TB(JDK17)
适用堆内存范围 4GB~64GB 8GB以上,大堆场景优势更明显
吞吐量表现 高,比ZGC高10%~15% 较高,比G1低10%~15%
调优复杂度 中等,需调整多个核心参数 极低,自适应能力极强
生产就绪时间 JDK9正式GA,默认收集器 JDK15正式GA,JDK17 LTS生产可用

4.2 生产环境选型指南

优先选择G1的场景
  1. 堆内存小于8GB的中小型应用场景

  2. 系统更看重吞吐量,而非极致的低延迟,如离线计算、批处理系统

  3. 写操作远多于读操作的业务场景

  4. 老系统升级JDK17,无需大幅调整,G1作为默认收集器兼容性最好

  5. CPU资源受限,核心数小于8核的服务器,ZGC的读屏障开销会被放大

优先选择ZGC的场景
  1. 堆内存大于8GB,尤其是32GB以上的大堆场景,ZGC的优势呈指数级放大

  2. 系统对延迟有严格要求,如金融交易、实时计算、微服务网关、广告投放系统,要求P999响应时间在10ms以内

  3. 读操作远多于写操作的业务场景

  4. 系统需要应对突发流量,避免STW导致的RT飙升、服务超时

  5. 未来计划升级到JDK21+,分代ZGC会进一步提升吞吐量,降低GC频率

五、生产级GC调优最佳实践与踩坑指南

5.1 通用GC调优黄金法则

  1. 必须开启GC日志 :生产环境必须保留完整的GC日志,JDK17推荐参数:-Xlog:gc*:file=/var/log/gc.log:time,uptime:filecount=10,filesize=100M,用于问题排查与调优分析

  2. 堆内存设置原则:Xms与Xmx必须设置为相同值,避免堆扩容的性能开销;堆内存建议设置为系统可用内存的50%~70%,不可超过80%,预留足够的内存给操作系统与元空间

  3. 禁止手动设置年轻代大小 :不要手动设置-Xmn参数,G1与ZGC的停顿预测模型依赖年轻代的动态调整,手动设置会覆盖自适应机制,导致停顿时间不可控

  4. 优先使用JVM自适应机制:不要盲目手动设置大量参数,绝大多数场景下,JVM的默认自适应参数已经足够优秀,仅需调整核心的堆内存、停顿时间目标、GC线程数三个参数

  5. 调优必须有基准测试:每次仅调整一个参数,对比调优前后的GC指标、系统吞吐量、响应时间,避免盲目调优导致的性能退化

5.2 G1收集器常见踩坑指南

  1. 巨型对象坑:大对象超过Region的50%,被分配到Humongous区,导致老年代内存快速占用,触发频繁GC甚至Full GC。解决方案:调整Region大小,优化代码减少大对象的创建,避免频繁生成大的临时数组、字符串

  2. MaxGCPauseMillis设置过小:设置值低于100ms,导致年轻代Region数量被压缩,Young GC频率飙升,吞吐量下降,甚至触发Full GC。解决方案:合理设置停顿目标,默认200ms,不建议低于100ms

  3. IHOP阈值设置过高:导致并发标记触发太晚,老年代内存已满,并发标记尚未完成,触发Full GC。解决方案:大对象较多的场景,下调IHOP阈值,提前触发并发标记

  4. 手动设置分代比例 :手动设置-XX:NewRatio等参数,覆盖G1的自适应分代机制,导致停顿时间不可控。解决方案:禁止手动设置分代比例,让JVM自动调整

5.3 ZGC收集器常见踩坑指南

  1. 堆内存设置过小:ZGC需要更多的内存缓冲空间,堆内存设置小于峰值存活对象的2倍,会导致频繁的Allocation Stall。解决方案:堆内存至少设置为峰值存活对象的2倍以上

  2. 并发GC线程数设置过少 :导致并发GC速度跟不上对象分配速度,出现分配停顿。解决方案:设置ConcGCThreads为CPU核心数的1/4,大流量场景可上调至1/3

  3. 启用预览版分代ZGC:JDK17的分代ZGC为预览版,生产环境使用会出现稳定性问题。解决方案:JDK17使用默认的单代ZGC,升级到JDK21+再启用分代ZGC

  4. 关闭压缩指针 :ZGC依赖压缩指针实现染色指针,手动关闭UseCompressedOops会导致ZGC无法正常工作。解决方案:保持压缩指针默认开启,禁止手动关闭

六、GC调优实战Demo

6.1 项目环境与依赖

  • JDK版本:JDK 17

  • 项目管理:Maven

  • 核心框架:Spring Boot 3.2.4

  • 持久层框架:MyBatis-Plus 3.5.6

  • 接口文档:Swagger3(SpringDoc)

  • 工具类:Spring Utils、Guava、FastJSON2

  • 数据库:MySQL 8.0

6.2 Maven pom.xml配置

复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.4</version>
        <relativePath/>
    </parent>
    <groupId>com.jam.demo</groupId>
    <artifactId>gc-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>gc-demo</name>
    <description>GC调优实战Demo</description>
    <properties>
        <java.version>17</java.version>
        <mybatis-plus.version>3.5.6</mybatis-plus.version>
        <fastjson2.version>2.0.52</fastjson2.version>
        <guava.version>33.1.0-jre</guava.version>
        <lombok.version>1.18.30</lombok.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <version>2.5.0</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>${fastjson2.version}</version>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>${guava.version}</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

6.3 MySQL建表语句(MySQL 8.0)

复制代码
CREATE TABLE `gc_test_data` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `data_content` text COMMENT '数据内容',
  `data_size` int DEFAULT NULL COMMENT '数据大小',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`),
  KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='GC测试数据表';

6.4 项目核心代码

启动类 GcDemoApplication.java
复制代码
package com.jam.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.info.Info;

/**
 * GC调优实战Demo启动类
 * @author ken
 * @date 2026-03-13
 */
@SpringBootApplication
@OpenAPIDefinition(info = @Info(title = "GC调优实战Demo", version = "1.0", description = "G1与ZGC调优测试接口"))
public class GcDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(GcDemoApplication.class, args);
    }

}
实体类 GcTestData.java
复制代码
package com.jam.demo.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;

/**
 * GC测试数据实体类
 * @author ken
 * @date 2026-03-13
 */
@Data
@TableName("gc_test_data")
public class GcTestData implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 主键ID
     */
    @TableId(type = IdType.AUTO)
    private Long id;

    /**
     * 数据内容
     */
    private String dataContent;

    /**
     * 数据大小
     */
    private Integer dataSize;

    /**
     * 创建时间
     */
    private LocalDateTime createTime;

}
Mapper接口 GcTestDataMapper.java
复制代码
package com.jam.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.GcTestData;
import org.apache.ibatis.annotations.Mapper;

/**
 * GC测试数据Mapper接口
 * @author ken
 * @date 2026-03-13
 */
@Mapper
public interface GcTestDataMapper extends BaseMapper<GcTestData> {

}
Service接口 GcTestService.java
复制代码
package com.jam.demo.service;

import com.jam.demo.entity.GcTestData;
import java.util.List;

/**
 * GC测试服务接口
 * @author ken
 * @date 2026-03-13
 */
public interface GcTestService {

    /**
     * 生成测试数据,触发GC
     * @param count 生成数据条数
     * @param dataSize 每条数据大小(字节)
     * @return 生成的数据列表
     */
    List<GcTestData> generateTestData(int count, int dataSize);

    /**
     * 批量保存测试数据
     * @param dataList 数据列表
     * @return 保存成功条数
     */
    int batchSaveData(List<GcTestData> dataList);

    /**
     * 清理测试数据
     * @return 清理成功条数
     */
    int clearTestData();

}
Service实现类 GcTestServiceImpl.java
复制代码
package com.jam.demo.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.google.common.collect.Lists;
import com.jam.demo.entity.GcTestData;
import com.jam.demo.mapper.GcTestDataMapper;
import com.jam.demo.service.GcTestService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.util.CollectionUtils;

import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;

/**
 * GC测试服务实现类
 * @author ken
 * @date 2026-03-13
 */
@Slf4j
@Service
public class GcTestServiceImpl implements GcTestService {

    private final GcTestDataMapper gcTestDataMapper;
    private final PlatformTransactionManager transactionManager;

    public GcTestServiceImpl(GcTestDataMapper gcTestDataMapper, PlatformTransactionManager transactionManager) {
        this.gcTestDataMapper = gcTestDataMapper;
        this.transactionManager = transactionManager;
    }

    @Override
    public List<GcTestData> generateTestData(int count, int dataSize) {
        if (count <= 0 || dataSize <= 0) {
            log.warn("生成测试数据参数非法,count:{}, dataSize:{}", count, dataSize);
            return Lists.newArrayList();
        }
        List<GcTestData> dataList = Lists.newArrayListWithCapacity(count);
        byte[] baseData = new byte[dataSize];
        for (int i = 0; i < dataSize; i++) {
            baseData[i] = (byte) (UUID.randomUUID().toString().charAt(i % 36));
        }
        String baseContent = new String(baseData, StandardCharsets.UTF_8);
        for (int i = 0; i < count; i++) {
            GcTestData testData = new GcTestData();
            testData.setDataContent(baseContent + UUID.randomUUID());
            testData.setDataSize(dataSize);
            testData.setCreateTime(LocalDateTime.now());
            dataList.add(testData);
        }
        log.info("生成测试数据完成,条数:{}, 单条数据大小:{}字节", count, dataSize);
        return dataList;
    }

    @Override
    public int batchSaveData(List<GcTestData> dataList) {
        if (CollectionUtils.isEmpty(dataList)) {
            log.warn("批量保存数据列表为空");
            return 0;
        }
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        def.setName("batchSaveDataTransaction");
        def.setPropagationBehavior(DefaultTransactionDefinition.PROPAGATION_REQUIRED);
        TransactionStatus status = transactionManager.getTransaction(def);
        int saveCount = 0;
        try {
            List<List<GcTestData>> partitionList = Lists.partition(dataList, 1000);
            for (List<GcTestData> partition : partitionList) {
                saveCount += gcTestDataMapper.insert(partition);
            }
            transactionManager.commit(status);
            log.info("批量保存数据完成,成功保存条数:{}", saveCount);
        } catch (Exception e) {
            transactionManager.rollback(status);
            log.error("批量保存数据失败", e);
            throw new RuntimeException("批量保存数据失败", e);
        }
        return saveCount;
    }

    @Override
    public int clearTestData() {
        LambdaQueryWrapper<GcTestData> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.gt(GcTestData::getId, 0);
        int deleteCount = gcTestDataMapper.delete(queryWrapper);
        log.info("清理测试数据完成,清理条数:{}", deleteCount);
        return deleteCount;
    }

}
测试接口 GcTestController.java
复制代码
package com.jam.demo.controller;

import com.jam.demo.entity.GcTestData;
import com.jam.demo.service.GcTestService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * GC测试接口
 * @author ken
 * @date 2026-03-13
 */
@Slf4j
@RestController
@RequestMapping("/api/gc")
@Tag(name = "GC测试接口", description = "用于G1、ZGC收集器调优测试的接口")
public class GcTestController {

    private final GcTestService gcTestService;

    public GcTestController(GcTestService gcTestService) {
        this.gcTestService = gcTestService;
    }

    @PostMapping("/generate")
    @Operation(summary = "生成测试数据", description = "生成指定数量和大小的测试数据,触发GC,用于调优测试")
    public String generateTestData(
            @Parameter(description = "生成数据条数", required = true) @RequestParam int count,
            @Parameter(description = "每条数据大小(字节)", required = true) @RequestParam int dataSize) {
        if (count <= 0 || count > 100000) {
            return "条数必须在1~100000之间";
        }
        if (dataSize <= 0 || dataSize > 1024 * 1024) {
            return "单条数据大小必须在1~1MB之间";
        }
        List<GcTestData> dataList = gcTestService.generateTestData(count, dataSize);
        return "生成测试数据成功,条数:" + dataList.size() + ",单条大小:" + dataSize + "字节";
    }

    @PostMapping("/save")
    @Operation(summary = "生成并保存测试数据", description = "生成并批量保存测试数据,触发GC,用于Full GC测试")
    public String saveTestData(
            @Parameter(description = "生成数据条数", required = true) @RequestParam int count,
            @Parameter(description = "每条数据大小(字节)", required = true) @RequestParam int dataSize) {
        if (count <= 0 || count > 100000) {
            return "条数必须在1~100000之间";
        }
        if (dataSize <= 0 || dataSize > 1024 * 1024) {
            return "单条数据大小必须在1~1MB之间";
        }
        List<GcTestData> dataList = gcTestService.generateTestData(count, dataSize);
        int saveCount = gcTestService.batchSaveData(dataList);
        return "保存测试数据成功,成功保存条数:" + saveCount;
    }

    @DeleteMapping("/clear")
    @Operation(summary = "清理测试数据", description = "清理所有测试数据,释放内存")
    public String clearTestData() {
        int deleteCount = gcTestService.clearTestData();
        return "清理测试数据成功,清理条数:" + deleteCount;
    }

    @PostMapping("/trigger")
    @Operation(summary = "手动触发GC", description = "手动调用System.gc(),触发Full GC,用于测试")
    public String triggerGc() {
        log.info("手动触发GC开始");
        System.gc();
        log.info("手动触发GC完成");
        return "手动触发GC完成";
    }

}
application.yml配置文件
复制代码
spring:
  application:
    name: gc-demo
  datasource:
    url: jdbc:mysql://localhost:3306/gc_demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
  main:
    banner-mode: off
mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true
  global-config:
    db-config:
      id-type: auto
springdoc:
  swagger-ui:
    path: /swagger-ui.html
    enabled: true
  api-docs:
    enabled: true
server:
  port: 8080

6.5 JVM启动参数示例

G1收集器启动参数(JDK17)
复制代码
-Xms10G -Xmx10G
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:InitiatingHeapOccupancyPercent=45
-XX:+UseCompressedOops
-XX:+UseCompressedClassPointers
-Xlog:gc*:file=/var/log/gc-g1.log:time,uptime:filecount=10,filesize=100M
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/heapdump.hprof
ZGC收集器启动参数(JDK17)
复制代码
-Xms16G -Xmx16G
-XX:+UseZGC
-XX:MaxGCPauseMillis=1
-XX:ConcGCThreads=4
-XX:+UseCompressedOops
-Xlog:gc*:file=/var/log/gc-zgc.log:time,uptime:filecount=10,filesize=100M
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/heapdump.hprof

七、总结

G1与ZGC是JDK17生态中最主流的两款垃圾收集器,G1作为默认收集器,兼顾吞吐量与可控的延迟,适配绝大多数企业级应用场景;ZGC作为革命性的低延迟收集器,将GC停顿控制在1ms以内,完美适配对延迟有严格要求的核心业务系统。

相关推荐
qq_404265832 小时前
Django全栈开发入门:构建一个博客系统
jvm·数据库·python
moonlight03043 小时前
运行时数据区
jvm
m0_528174454 小时前
使用Python处理计算机图形学(PIL/Pillow)
jvm·数据库·python
一瓢西湖水5 小时前
CPU使用超过阈值分析
java·开发语言·jvm
不知名。。。。。。。。5 小时前
仿muduo库实现高并发服务器----EventLoop与线程整合起来
java·开发语言·jvm
xixihaha13245 小时前
使用Flask快速搭建轻量级Web应用
jvm·数据库·python
xixihaha13245 小时前
实战:用OpenCV和Python进行人脸识别
jvm·数据库·python
一直学习的程序小白5 小时前
java进阶-优化GC垃圾回收机制
java·开发语言·jvm