深入解析JVM八大垃圾回收器:从原理到实战调优
1. 项目概述为什么我们需要八种不同的“清洁工”如果你写过Java程序大概率见过OutOfMemoryError这个老朋友。它就像一个不请自来的客人总是在你最不希望它出现的时候登门拜访。问题的根源往往不在于你写的代码逻辑而在于代码运行的那个“家”——JVMJava虚拟机的管理机制。这个“家”里的内存空间是有限的当创建的对象占满了空间而一些无用的“垃圾”对象又没有被及时清理时内存就会告急。负责清理这些垃圾的就是垃圾回收器Garbage Collector, GC。但为什么会有Serial、Parallel、CMS、G1、ZGC这么多种呢这就像城市保洁一个宁静的居民小区一个保洁员慢悠悠地打扫就够了但换到人潮汹涌的火车站就必须出动一个保洁团队甚至需要分区域、分时段进行高效作业。不同的垃圾回收器正是为了应对不同规模、不同特点的“城市”即应用场景而设计的。理解它们不是为了应付面试而是为了在真实的生产环境中当你的应用出现卡顿、延迟飙升甚至服务崩溃时你能精准地定位到是哪个“清洁工”不给力并知道如何为它更换更合适的“工具”或调整它的“工作流程”。这篇文章我将结合自己多年在Java后端性能调优中踩过的坑带你彻底搞懂这八种主流JVM垃圾回收器的核心原理、适用场景和调优关键让你不仅能说出它们的名字更能理解它们背后的设计哲学和实战选择。2. 垃圾回收基础理解“标记”与“清除”在深入各个回收器之前我们必须统一语言理解垃圾回收的两个最基础、最核心的动作标记Mark和清除Sweep。几乎所有现代垃圾回收器的工作流程都是这两个动作的变体与组合。标记顾名思义就是找出哪些对象是“垃圾”。JVM如何判断一个对象已经“死了”呢它采用的是“可达性分析算法”。想象一下你的Java程序里有一堆对象它们之间通过引用相互连接形成一张复杂的网。JVM会定义一组“根”GC Roots比如虚拟机栈中引用的对象、静态属性引用的对象、常量引用的对象等。从这些“根”出发能够遍历到的所有对象都被认为是“存活”的就像从树根能长到的所有枝叶。而那些从任何“根”都无法到达的对象就被判定为“垃圾”是需要被回收的。这个过程就是标记阶段。清除就是在标记完成后将那些被判定为垃圾的对象所占用的内存空间回收以便后续分配新对象使用。最简单的清除方式就是“标记-清除”Mark-Sweep直接把垃圾对象的内存标记为空闲。但这种方式会产生内存碎片——空闲内存东一块西一块不连续。当需要分配一个较大的对象时可能找不到一块足够大的连续内存即使总空闲内存还很多也会导致分配失败从而触发另一次垃圾回收。为了解决碎片问题引入了“标记-整理”Mark-Compact和“复制”Copying算法。标记-整理在标记存活对象后将所有存活的对象向内存空间的一端“滑动”紧凑地排列在一起然后直接清理掉边界以外的所有内存。这样整理后空闲内存就是连续的一大块。复制将内存分为大小相等的两块From和To。分配新对象时只使用其中一块From。当这块内存快用完时就触发GC将From区中所有存活的对象复制到另一块空闲的To区并且紧凑地排列好然后一次性清空整个From区。之后From和To角色互换。注意理解这些基础算法至关重要因为后续所有回收器的差异本质上都是对这些基础算法在不同维度如单线程/多线程、分代/不分代、暂停时间/吞吐量上的组合与优化。例如Parallel Scavenge关注的是“吞吐量”它可能更倾向于高效的复制算法来快速清理年轻代而CMS和G1关注“低延迟”它们的设计中就需要更精细地平衡标记、清除和整理的过程避免长时间的“停顿”。3. 分代收集理论JVM内存管理的核心策略为什么JVM要把堆内存划分成年轻代Young Generation和老年代Old Generation这源于一个在大多数Java应用中观察到的经验规律绝大多数对象的生命周期都非常短暂。这个规律被称为“弱分代假说”。基于这个规律JVM采用了“分代收集”策略。它将堆内存逻辑上划分为两个区域年轻代新创建的对象首先被分配在这里。因为对象“朝生夕死”的特性年轻代会发生非常频繁的垃圾回收这种回收被称为Minor GC或Young GC。年轻代内部又分为一个Eden区和两个Survivor区S0和S1采用的就是上面提到的“复制”算法。对象在Eden区诞生经过一次Minor GC后存活的对象会被复制到其中一个Survivor区年龄加1。在Survivor区中经过多次Minor GC依然存活的对象默认年龄阈值是15会被晋升Promote到老年代。老年代存放那些经历了多次Minor GC后依然存活的对象以及一些大对象可能直接分配在老年代。老年代的对象生命周期较长因此垃圾回收发生的频率远低于年轻代但一旦发生需要处理的对象量通常很大耗时也更长。这种回收被称为Major GC或Full GC。老年代通常采用“标记-清除”或“标记-整理”算法。分代收集的好处是显而易见的可以对不同“年龄”的对象施以不同的收集策略用较小的代价回收掉年轻代中大量的“垃圾”从而整体上提升GC效率。我们接下来要讲的所有回收器除了ZGC和Shenandoah等较新的几乎都是基于分代模型设计的。4. 八种垃圾回收器深度解析现在让我们进入正题逐一拆解这八种垃圾回收器。我会按照它们大致出现的时间顺序和设计理念的演进进行讲解并重点说明其工作原理、优缺点和典型应用场景。4.1 Serial 收集器最古老的单线程卫士核心特点单线程、串行化。进行垃圾回收时必须暂停所有用户线程Stop The World, STW直到回收结束。工作区域年轻代采用复制算法老年代采用标记-整理算法。启用参数-XX:UseSerialGCSerial收集器是JVM最基础、历史最悠久的收集器。它的“单线程”不仅仅指它使用一个CPU或一条线程去完成垃圾收集更重要的是在它工作时必须暂停所有其他工作线程就像让整个程序世界静止等它一个人打扫完。为什么现在还有用听起来极其低效但它有不可替代的优势简单而高效。在单核CPU的桌面客户端场景或者内存资源非常有限的嵌入式系统、微服务场景下它没有线程交互的开销专心做收集反而能获得很高的单线程收集效率。对于许多小型应用或测试环境它依然是默认或不错的选择。它的存在提醒我们没有最好的收集器只有最适合场景的收集器。实操心得如果你在开发一个客户端桌面工具比如一个IDE插件或者一个内存需求极小、功能简单的后台服务可以尝试使用Serial收集器。你可能会惊讶于它的稳定性和低开销。但在任何多核服务器上部署核心在线服务绝对不要用它。4.2 ParNew 收集器Serial的多线程并行版核心特点Serial收集器的多线程并行版本。除了使用多线程进行垃圾收集外其余行为如STW、采用的算法、内存分配规则等与Serial收集器完全一致。工作区域主要是年轻代复制算法。启用参数-XX:UseParNewGCParNew的出现是为了充分利用多核CPU的计算能力。在年轻代收集时它使用多个GC线程并行清理可以显著缩短年轻代GC的停顿时间。但它依然是“并行”而非“并发”收集阶段依然需要STW。关键搭配在JDK 9之前ParNew有一个重要的“搭档”——CMSConcurrent Mark Sweep收集器。因为CMS是老年代收集器它需要与一个年轻代收集器配合工作而ParNew是唯一能与CMS配合的年轻代并行收集器。所以参数-XX:UseConcMarkSweepGC默认会激活ParNew CMS的组合。注意事项在单核CPU环境下ParNew的性能可能反而不如Serial因为线程切换有开销。另外它默认开启的收集线程数与CPU核心数相同可以通过-XX:ParallelGCThreads参数调整。4.3 Parallel Scavenge 收集器吞吐量优先的实干家核心特点关注点是吞吐量Throughput即CPU用于运行用户代码的时间与CPU总消耗时间的比值。目标是在可控的停顿时间内达到最高的吞吐量。工作区域年轻代复制算法。启用参数-XX:UseParallelGCParallel Scavenge 和 ParNew 看起来很像都是多线程并行收集年轻代。但它们的设计目标有本质区别。ParNew的目标是尽可能缩短单次GC的停顿时间适合需要快速响应的交互式应用。而Parallel Scavenge的目标是达到一个可控制的吞吐量适合后台运算、批处理等不太关心单次停顿但追求总体任务尽快完成的应用。它提供了两个关键参数用于精确控制吞吐量行为-XX:MaxGCPauseMillis设置GC最大停顿时间的期望值注意是期望值并非硬性保证。收集器会尝试调整堆大小等参数来满足这个期望但可能会以降低吞吐量为代价。-XX:GCTimeRatio直接设置吞吐量目标值是一个大于0小于100的整数代表GC时间占总时间的比率。例如设置为99表示希望GC时间不超过总时间的1%。调优技巧对于数据分析、科学计算等离线任务使用Parallel Scavenge并设置合理的GCTimeRatio比如95或99让JVM自动去平衡堆大小和GC频率往往能获得最佳的整体运行效率。不要过度纠结某一次GC停了200毫秒还是300毫秒。4.4 Parallel Old 收集器Parallel Scavenge的老年代搭档核心特点Parallel Scavenge收集器的老年代版本使用多线程并行和“标记-整理”算法。工作区域老年代。启用参数-XX:UseParallelOldGC在Parallel Old出现之前Parallel Scavenge只能与Serial OldSerial的老年代版搭配这形成了一个“吞吐量优先的年轻代”配上一个“慢吞吞的单线程老年代”的尴尬组合在Full GC时会产生长时间停顿严重拖累整体吞吐量。Parallel Old的诞生补全了这块拼图。现在使用-XX:UseParallelGC或-XX:UseParallelOldGC都会激活Parallel Scavenge Parallel Old这个组合。这是一个真正意义上全程关注吞吐量的收集器组合在JDK 8及之前它是服务端默认的GC组合如果未显式指定其他GC。应用场景适用于注重吞吐量、可接受较长停顿如Full GC可能持续数秒的后台计算型应用。例如Hadoop离线MapReduce作业、定时跑批的报表生成服务等。4.5 CMS 收集器以最短停顿时间为目标的里程碑核心特点第一款真正意义上追求低延迟的收集器其核心是在老年代收集过程中让垃圾收集线程与用户线程并发工作以最大限度减少STW时间。工作区域老年代。采用“标记-清除”算法。启用参数-XX:UseConcMarkSweepGC(这会自动使用ParNew作为年轻代收集器)CMS的收集过程相对复杂分为四个主要阶段初始标记STW。仅仅标记一下GC Roots能直接关联到的对象速度极快。并发标记并发执行。从GC Roots的直接关联对象开始遍历整个对象图。这个过程耗时较长但与应用线程一起运行不会停顿。重新标记STW。修正并发标记期间因用户线程继续运行而导致标记产生变动的那一部分对象。这个停顿比初始标记长但远比并发标记的整个时间段短。并发清除并发执行。清理掉标记阶段判定为死亡的对象。可以看到CMS把最耗时的“标记”和“清除”过程做到了与用户线程并发只在初始标记和重新标记时需要短暂的停顿。这使其在JDK 1.5-1.8时代成为许多对响应时间敏感的互联网Java应用如Web服务器的首选。然而CMS有三大显著缺点对CPU资源敏感并发阶段会占用一部分线程CPU资源导致应用程序吞吐量降低。默认启动的回收线程数是(CPU核心数 3) / 4在CPU核心数不足4个时对应用影响较大。无法处理“浮动垃圾”在并发清除阶段用户线程还在运行可能会产生新的垃圾对象这些对象出现在标记过程之后本次GC无法处理只能留到下次GC这就是“浮动垃圾”。因此CMS不能像其他收集器那样等到老年代快满了再收集需要预留一部分空间 (-XX:CMSInitiatingOccupancyFraction设置触发百分比如68%) 供并发收集时程序运行使用。如果预留空间不足就会引发“并发模式失败”此时JVM会临时启用Serial Old收集器来进行一次Full GC导致长时间停顿。内存碎片由于使用“标记-清除”算法会产生内存碎片。当无法找到足够大的连续空间分配大对象时也会触发Full GC。CMS提供了-XX:UseCMSCompactAtFullCollection默认开启在Full GC时进行碎片整理以及-XX:CMSFullGCsBeforeCompaction设置多少次不压缩的Full GC后才执行一次压缩。避坑指南使用CMS时必须密切监控老年代使用率和并发模式失败的频率。合理设置CMSInitiatingOccupancyFraction是关键设置太高容易引发并发失败设置太低又会频繁触发CMS收集。通常建议在70%-75%左右开始监控和调整。4.6 G1 收集器面向服务端应用的全功能选手核心特点面向服务端、可预测停顿时间的垃圾收集器。它摒弃了传统的连续物理分代将堆划分为多个大小相等的独立区域Region并跟踪每个Region的垃圾堆积“价值”回收所需时间与回收所得空间的比值在后台维护一个优先级列表优先回收价值最大的Region。工作区域整个堆逻辑上仍分代但物理上不连续。启用参数-XX:UseG1GCG1的设计目标是替代CMS它同时兼顾了吞吐量和低延迟。从JDK 9开始G1成为了服务端模式下的默认垃圾收集器。核心原理Region分区将堆划分为约2048个大小在1MB到32MB之间可调的Region。每个Region可以是Eden、Survivor、Old或Humongous巨型对象区用于存放超过Region一半大小的对象中的一种。收集集合G1的回收不是以整个年轻代或老年代为单位而是以收集集合为单位。每次回收G1会根据用户设定的停顿时间目标-XX:MaxGCPauseMillis默认200ms选择若干个回收价值最高的Region组成收集集合进行回收。这保证了每次回收的停顿时间都在可控范围内。回收过程G1的回收过程类似于CMS但更精细。年轻代收集STW采用复制算法将Eden和Survivor区的存活对象复制到新的Survivor区或老年代Region。并发标记周期一个全局性的标记过程类似于CMS但标记的是整个堆。它为老年代回收Mixed GC提供依据。混合收集在并发标记周期结束后G1知道哪些老年代Region含有最多垃圾。接下来的几次收集不仅会收集所有年轻代Region还会根据停顿目标选择一部分垃圾多的老年代Region加入收集集合这就是“混合收集”。通过多次混合收集逐步将老年代的垃圾清理干净。G1 vs CMS 优势可预测的停顿通过设定目标停顿时间G1能更精确地控制每次GC的停顿。空间整合整体上采用“标记-整理”算法Region之间采用复制算法两种算法都意味着不会产生内存碎片。更精细的回收避免了CMS在堆内存较大时老年代回收的不确定性。调优关键-XX:MaxGCPauseMillis这是最重要的参数。但请注意G1会尽力达成目标而不是保证。设置一个不切实际的小值如20ms会导致G1频繁进行小规模回收反而降低吞吐量。-XX:G1HeapRegionSize设置Region大小。一般不用手动设置G1会根据堆大小自动计算。-XX:InitiatingHeapOccupancyPercent触发并发标记周期的堆占用率阈值默认45%。4.7 ZGC 收集器超低延迟的下一代王者核心特点一款在JDK 11中引入的实验性、可扩展的低延迟垃圾收集器。其设计目标是将STW停顿时间控制在10毫秒以内且停顿时间不会随着堆大小或活跃对象集的增大而显著增加。工作区域整个堆逻辑分代物理不分代。从JDK 15开始成为正式特性。启用参数-XX:UseZGCZGC的实现非常复杂其核心是两大关键技术着色指针和读屏障。着色指针ZGC在64位指针中借用了几个高位比特来存储对象的元数据如标记信息、重定位信息。这样GC相关的状态信息就与对象指针本身绑定在一起无需额外的内存数据结构来记录减少了访问开销。读屏障这是ZGC实现并发转移对象移动时用户线程无需停顿的关键。当应用程序线程从堆中读取对象引用时会经过一个“屏障”这个屏障代码会检查指针的元数据如果发现对象正在被转移则线程会“帮忙”完成转移工作并更新指针。这个过程对用户线程是透明的且停顿极短。ZGC的工作阶段与用户线程高度并发并发标记类似CMS/G1。并发预备重分配确定本次回收要清理哪些Region。并发重分配核心阶段。将存活对象从待回收的Region复制到新的Region。这是ZGC与G1/CMS最大的不同它通过读屏障实现了对象的并发转移从而消除了传统GC中耗时最长的“转移存活对象”阶段的STW。并发重映射修正堆中所有指向旧对象地址的指针使其指向新地址。优势与挑战优势亚毫秒级到十毫秒级的停顿几乎对应用无感吞吐量损失相比G1更小通常不超过15%支持TB级别的堆内存。挑战在JDK 17及之前版本ZGC不支持分代收集JDK 21引入了分代式ZGC的预览版。这意味着所有对象无论新旧都混在一起处理对于对象生命周期差异巨大的应用可能不如分代收集器高效。此外读屏障会带来一定的性能开销。适用场景对延迟极其敏感的应用如实时交易系统、高频量化交易、大型在线游戏服务器等。当你的应用P99响应时间要求极高且堆内存较大数十GB以上时ZGC是首选。4.8 Shenandoah 收集器与ZGC并驾齐驱的低延迟竞争者核心特点由Red Hat主导开发与ZGC目标类似也是一款低停顿时间的垃圾收集器。它甚至宣称在某些场景下停顿时间比ZGC更短。工作区域整个堆逻辑分代物理不分代。启用参数-XX:UseShenandoahGCShenandoah的实现原理与ZGC有相似之处也使用了转发指针和读屏障来实现并发压缩。但它与ZGC在实现细节上有所不同例如其内存布局和屏障的具体实现。Shenandoah vs ZGC 简要对比性能两者停顿时间都极低在不同基准测试和实际应用中互有胜负没有绝对优劣。Shenandoah的并发压缩算法可能在某些特定工作负载下略有优势。兼容性ZGC是Oracle JDK的“亲儿子”从JDK 11开始引入15正式化后续支持力度大。Shenandoah最初是OpenJDK的组件但Oracle JDK 12也包含了它不过可能需要额外参数开启。在Red Hat的OpenJDK发行版中Shenandoah是重点。成熟度两者都是生产可用的。选择哪一个更多取决于你的JDK发行版、社区支持以及针对自己应用的实际压测结果。选择建议如果你在使用Red Hat系的环境如OpenShift可以优先考虑Shenandoah。否则在Oracle JDK上ZGC的集成度和官方文档支持可能更好。最好的方式是在预生产环境对两者进行对比压测。5. 如何根据场景选择垃圾回收器了解了所有回收器的特点后选择就变成了一个匹配需求的技术决策。下面这个表格和决策流可以帮你快速定位收集器目标适用场景关键参数/注意事项Serial简单高效单线程客户端模式、微服务、嵌入式、测试-XX:UseSerialGCParNew降低年轻代停顿与CMS搭配使用JDK 8及以前-XX:UseParNewGC注意CPU核心数Parallel Scavenge/Old高吞吐量后台计算、批处理、科学运算-XX:UseParallelGC,-XX:MaxGCPauseMillis,-XX:GCTimeRatioCMS低延迟老年代JDK 8及以前的对响应敏感的服务端应用-XX:UseConcMarkSweepGC关注并发失败和碎片G1可预测停顿平衡吞吐与延迟JDK 8 的主流服务端应用堆内存4G-6G以上-XX:UseG1GC,-XX:MaxGCPauseMillis200ZGC超低延迟大堆JDK 11 的对延迟极度敏感的应用超大堆-XX:UseZGC,-Xmx设置大堆Shenandoah超低延迟与ZGC类似Red Hat环境或需对比测试-XX:UseShenandoahGC决策流程图你的应用是客户端/微型服务吗是 - 考虑Serial。你的应用是后台计算追求总任务完成时间最短吗是 - 选择Parallel Scavenge/Old。你的应用是在线服务关注请求响应时间吗如果使用JDK 8堆内存小于4G可考虑ParNew CMS大于4G可评估G1。如果使用JDK 11优先使用G1默认。你的应用对延迟有极端要求P99 10ms且堆内存很大32G吗是 - 升级到JDK 11并测试ZGC或Shenandoah。重要提示任何理论选择都必须经过实际压测验证在预生产环境使用相同的流量模型对比不同GC下的关键指标平均响应时间、P99/P999响应时间、系统吞吐量TPS/QPS、GC停顿时间通过GC日志分析。眼见为实数据是选择的最终依据。6. 实战GC日志分析与关键参数调优知道选哪个收集器只是第一步更关键的是如何验证它的表现并做精细调整。这一切都始于分析GC日志。开启并读懂GC日志 在JVM启动参数中添加以下参数开启详细的GC日志-Xlog:gc*:filegc.log:time,uptime,level,tags:filecount10,filesize100M对于JDK 9 使用统一日志框架对于JDK 8及以前使用-XX:PrintGCDetails -XX:PrintGCDateStamps -Xloggc:gc.log一份G1的GC日志片段可能如下[0.711s][info][gc,start ] GC(0) Pause Young (Normal) (G1 Evacuation Pause) [0.711s][info][gc,task ] GC(0) Using 8 workers [0.717s][info][gc,phases ] GC(0) Pre Evacuate Collection Set: 0.0ms [0.717s][info][gc,phases ] GC(0) Evacuate Collection Set: 5.8ms [0.717s][info][gc,phases ] GC(0) Post Evacuate Collection Set: 0.2ms [0.717s][info][gc,phases ] GC(0) Other: 0.1ms [0.717s][info][gc,heap ] GC(0) Eden regions: 25-0(25) [0.717s][info][gc,heap ] GC(0) Survivor regions: 0-4(4) [0.717s][info][gc,heap ] GC(0) Old regions: 0-1 [0.717s][info][gc,heap ] GC(0) Humongous regions: 0-0 [0.717s][info][gc,metaspace ] GC(0) Metaspace: 6505K-6505K(1056768K) [0.717s][info][gc ] GC(0) Pause Young (Normal) 200M-10M(1024M) 6.133ms你需要关注的关键信息Pause Young这是一次年轻代回收。200M-10M回收前堆使用量为200M回收后为10M。(1024M)当前堆的总容量。6.133ms本次GC的停顿时间。常用调优参数速查堆大小-Xms初始堆和-Xmx最大堆。务必设置成相同值以避免堆伸缩带来的性能开销。年轻代大小-Xmn如-Xmn2g。对于G1一般不建议手动设置由G1自动调节更好。元空间大小-XX:MetaspaceSize和-XX:MaxMetaspaceSize防止元空间动态调整引发Full GC。GC日志如上所述必须开启。Full GC前的手动触发-XX:DisableExplicitGC禁止代码中System.gc()触发Full GCNIO等框架可能会调用。大对象阈值-XX:PretenureSizeThresholdSerial/ParNew等大于此值的对象直接进入老年代。晋升年龄-XX:MaxTenuringThreshold控制对象晋升到老年代的年龄。调优心法先满足容量再优化延迟首先确保-Xmx足够大避免频繁的Full GC。通常建议设置为系统可用内存的70%-80%。优先使用默认值特别是G1、ZGC等现代收集器其自适应算法已经非常智能在未明确瓶颈前不要盲目调整Region大小、IHOP阈值等高级参数。关注核心指标通过监控系统如Prometheus Grafana持续观察应用吞吐量、响应时间P95, P99、GC频率、GC停顿时间、老年代使用率。调优的目标是让这些指标达到业务可接受的范围。一次只改一个参数记录每次变更前后的性能数据进行对比分析。7. 常见问题排查与案例实录在实际运维中GC问题千奇百怪但大多可以归为以下几类。这里分享几个我亲身处理的案例和排查思路。问题一应用周期性卡顿监控显示有规律的长时间STW。现象每过几分钟应用响应时间就飙升一次持续几秒到十几秒。排查查看GC日志发现每次卡顿时都伴随着一次Full GC。进一步分析发现老年代在Full GC前使用率都接近100%。根因对象晋升过快或存在内存泄漏导致老年代迅速被填满触发Full GC。解决如果是晋升过快检查-Xmn年轻代大小是否过小可以通过增大年轻代让对象在年轻代经历更多次GC过滤掉短期对象。同时检查-XX:MaxTenuringThreshold是否合理。如果是内存泄漏使用jmap -histo:live或jmap -dump:live,fileheap.bin导出堆内存快照用MAT或JVisualVM分析找出数量异常增长的对象类定位到创建该对象的代码。我的案例一个缓存服务误将本地缓存设置为永不过期且缓存键设计不当导致数量无限增长最终撑爆老年代。解决方案是引入合理的过期策略和缓存键范围限制。问题二CPU使用率长期偏高但系统吞吐量不高。现象服务器CPU使用率持续在80%以上但应用的QPS并不高。排查使用top -Hp查看进程内线程CPU占用发现多个GC线程消耗了大量CPU。查看GC日志发现GC频率异常高但每次回收掉的内存很少。根因对象分配速率过快或者存在大量“朝生夕死”的极小对象导致GC线程疲于奔命。解决优化代码避免在循环、高频调用方法中创建大量临时对象。对于分配速率过快可以考虑适当调大堆内存特别是年轻代降低GC频率。检查是否有不合理的日志打印如DEBUG级别日志在线上打印大对象、序列化/反序列化操作。我的案例一个消息处理服务在处理每条消息时都使用new SimpleDateFormat()来解析时间戳。SimpleDateFormat非线程安全且创建开销大导致大量短暂对象产生。将其改为静态变量或使用ThreadLocal包装后GC频率下降超过60%。问题三使用G1时停顿时间偶尔会远超设定的MaxGCPauseMillis。现象设置了-XX:MaxGCPauseMillis200但监控显示偶尔会出现超过500ms甚至1s的停顿。排查分析GC日志发现超长的停顿发生在Mixed GC阶段或者发生了Full GC。根因并发模式失败对象分配速率太快G1的并发标记速度跟不上导致老年代在标记完成前就被填满不得不退化为Full GC。巨型对象分配失败Humongous Region分配或回收效率低。元空间扩容Metaspace触发扩容导致的Full GC。解决对于原因1尝试增加堆内存或者通过-XX:ConcGCThreads适当增加并发标记线程数但会增加CPU开销。对于原因2审视业务代码避免分配过大的对象如超大数组。可以尝试调整-XX:G1HeapRegionSize使大多数对象不会成为巨型对象。对于原因3设置-XX:MetaspaceSize和-XX:MaxMetaspaceSize为固定值并预留足够空间。我的心得MaxGCPauseMillis是一个目标不是保证。G1会尽力达成但极端情况下降级是可能的。调优是一个平衡艺术在吞吐量和延迟之间寻找最佳点。问题四从CMS迁移到G1后吞吐量下降明显。现象应用从JDK 8 CMS 升级到 JDK 11 G1默认CPU使用率相似但处理能力TPS下降了10%-15%。排查这是正常现象。G1为了实现可预测的停顿引入了更复杂的后台线程和数据结构如RSet其CPU开销和内存开销通常比CMS要高一些这会略微牺牲一些吞吐量。权衡这是用一部分吞吐量换来了更稳定、可预测的延迟。对于大多数在线服务更平滑的响应曲线比绝对峰值吞吐量更重要。如果吞吐量下降在业务可接受范围内且P99延迟得到改善那么迁移就是成功的。优化可以尝试微调-XX:MaxGCPauseMillis稍微放宽停顿目标如从200ms调到250ms给G1更多空间来优化吞吐量。