资讯动态

JVM垃圾回收器深度解析:从算法原理到实战调优

发布时间:2026/8/16 18:44:14 来源:尧图企业网站定制
1. 项目概述为什么我们需要深入理解垃圾回收器如果你是一名Java开发者或者正在使用任何基于JVM的语言比如Kotlin、Scala那么“垃圾回收器”这个词对你来说一定不陌生。它就像是你程序背后那个默默无闻的清洁工负责回收那些不再被使用的内存对象防止内存泄漏确保应用能够稳定、持续地运行。但很多时候我们对它的认知可能仅仅停留在“它会自动回收内存”这个层面至于它具体怎么工作、有哪些种类、各自有什么优缺点、又该如何为我们的应用选择合适的“清洁工”很多人可能就一头雾水了。尤其是在面对生产环境的性能调优时比如应用出现频繁的Full GC导致服务卡顿或者堆内存设置不合理导致资源浪费如果你对GC的工作原理和不同回收器的特性没有深入的了解排查问题就会像盲人摸象非常被动。这篇文章的目的就是带你彻底搞懂JVM的垃圾回收器。我不会只停留在概念介绍而是会结合我这些年处理线上问题的实际经验从底层原理、算法实现到不同回收器的适用场景、核心参数调优再到实战中遇到的典型问题和排查技巧为你构建一个完整、立体的知识体系。无论你是刚入门的新手还是有一定经验想深入优化的开发者相信都能从中获得可以直接用于实践的“干货”。2. 垃圾回收的核心思想与算法基础在深入各个回收器之前我们必须先打好地基理解垃圾回收赖以生存的几个核心思想和基础算法。这是理解后续所有复杂回收器设计的前提。2.1 如何判定对象“已死”垃圾回收的首要任务是识别哪些内存对象是“垃圾”即已经不再被使用的对象。JVM主要依赖两种算法来进行判定引用计数算法这是一种非常直观的想法给对象添加一个引用计数器每当有一个地方引用它时计数器就加1当引用失效时计数器就减1。任何时刻计数器为0的对象就是不可能再被使用的。这个方法实现简单判定效率高但它有一个致命的缺陷无法解决对象之间循环引用的问题。比如对象A和对象B互相引用除此之外再无其他引用那么它们的引用计数都不为0但实际上它们已经无法被外界访问应该被回收。因此主流的Java虚拟机都没有选用引用计数算法来管理内存。可达性分析算法这是当前主流JVM包括HotSpot所采用的算法。它的基本思路是通过一系列称为“GC Roots”的根对象作为起始点从这些节点开始向下搜索搜索所走过的路径称为“引用链”。当一个对象到GC Roots没有任何引用链相连时则证明此对象是不可用的可以被回收。那么哪些对象可以作为GC Roots呢主要包括以下几种虚拟机栈栈帧中的本地变量表中引用的对象。方法区中类静态属性引用的对象。方法区中常量引用的对象。本地方法栈中JNI即Native方法引用的对象。Java虚拟机内部的引用如基本数据类型对应的Class对象常驻的异常对象NullPointerException、OutOfMemoryError等。所有被同步锁synchronized关键字持有的对象。可达性分析算法有效地解决了循环引用的问题因为循环引用的两个对象如果都无法到达GC Roots那么它们就会被判定为可回收对象。注意即使在可达性分析中被判定为不可达的对象也并非是“非死不可”的。要真正宣告一个对象死亡至少要经历两次标记过程。第一次标记后如果对象没有覆盖finalize()方法或者finalize()方法已经被虚拟机调用过虚拟机将直接进行回收。否则这个对象会被放置在一个名为F-Queue的队列中由一个低优先级的Finalizer线程去执行它的finalize()方法。这里对象可以获得“最后一次自救机会”只要在finalize()中重新与引用链上的任何一个对象建立关联即可。但强烈不建议依赖这个方法来做资源释放因为它的执行时机不确定且性能开销大。2.2 经典垃圾收集算法知道了哪些是垃圾接下来就要研究如何清理。主要有三种基础的收集算法后续所有复杂的垃圾回收器都是这些算法的组合或优化。标记-清除算法这是最基础的收集算法分为“标记”和“清除”两个阶段。首先标记出所有需要回收的对象在标记完成后统一回收所有被标记的对象。优点实现简单不需要移动对象。缺点1.效率问题标记和清除两个过程的效率都不高。2.空间问题标记清除后会产生大量不连续的内存碎片。空间碎片太多可能导致以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集。复制算法为了解决效率问题复制算法出现了。它将可用内存按容量划分为大小相等的两块每次只使用其中的一块。当这一块的内存用完了就将还存活着的对象复制到另外一块上面然后再把已使用过的内存空间一次清理掉。优点每次都是对整个半区进行内存回收实现简单运行高效而且不会产生内存碎片。缺点将可用内存缩小为了原来的一半空间浪费太大。现代的商业虚拟机都采用这种收集算法来回收新生代。因为新生代中的对象有98%是“朝生夕死”的所以并不需要按照1:1的比例来划分内存空间而是将内存分为一块较大的Eden空间和两块较小的Survivor空间通常比例是8:1:1。每次使用Eden和其中一块Survivor。当回收时将Eden和Survivor中存活的对象一次性复制到另一块Survivor上最后清理掉Eden和用过的Survivor。这样只有10%的内存会被“浪费”。标记-整理算法复制算法在对象存活率较高时比如老年代就要进行较多的复制操作效率会变低。更关键的是如果不想浪费50%的空间就需要有额外的空间进行分配担保以应对被使用的内存中所有对象都100%存活的极端情况。所以老年代一般不能直接选用复制算法。 标记-整理算法的标记过程与“标记-清除”一样但后续步骤不是直接对可回收对象进行清理而是让所有存活的对象都向内存空间的一端移动然后直接清理掉边界以外的内存。优点避免了内存碎片也避免了复制算法空间减半的代价。缺点移动存活对象并更新所有引用这些对象的地方“指针调整”是一种负重操作而且这种操作必须暂停用户线程Stop The World, STW才能进行对停顿时间敏感的应用不友好。分代收集理论当前商业虚拟机的垃圾收集器大多数都遵循了“分代收集”的理论。它建立在两个分代假说之上1.弱分代假说绝大多数对象都是朝生夕灭的。2.强分代假说熬过越多次垃圾收集过程的对象就越难以消亡。基于这两个假说收集器将Java堆划分出不同的区域新生代、老年代然后根据各个区域的特点采用不同的收集算法。新生代每次回收都有大量对象死去存活少量适合复制算法。老年代对象存活率高没有额外空间担保适合标记-清除或标记-整理算法。3. 七种经典垃圾回收器深度解析理解了基础算法我们就可以来看具体的“清洁工”——垃圾回收器了。HotSpot JVM提供了多种选择它们之间的关系并非替代而是各有侧重适用于不同的场景。下图展示了它们之间的配合关系注此处用文字描述替代图表新生代收集器Serial, ParNew, Parallel Scavenge老年代收集器Serial Old, Parallel Old, CMS整堆收集器G1, ZGC, Shenandoah (后两者为新一代低延迟收集器)其中有连线连接的表示它们可以搭配使用。例如Serial可以搭配Serial OldParNew可以搭配CMS。3.1 新生代收集器关注吞吐量还是响应时间Serial收集器这是一个单线程的收集器。它的“单线程”意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作更重要的是在它进行垃圾收集时必须暂停其他所有的工作线程Stop The World直到它收集结束。工作过程采用复制算法。优点简单而高效。对于限定单个CPU的环境来说由于没有线程交互的开销专心做垃圾收集自然可以获得最高的单线程收集效率。它是Client模式下虚拟机默认的新生代收集器。缺点STW时间可能较长。适用场景桌面应用或客户端小程序内存不大停顿时间可以接受。在服务器环境它主要用于虚拟机在后台管理、监控或日志输出的场景。ParNew收集器实质上是Serial收集器的多线程并行版本。除了使用多条线程进行垃圾收集之外其余行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样。工作过程采用复制算法多线程并行收集。优点在有多核CPU的环境下可以缩短垃圾收集的停顿时间。缺点在单核CPU环境下性能可能不如Serial因为存在线程交互的开销。适用场景Server模式下的虚拟机首选的新生代收集器一个重要原因是除了Serial目前只有它能与CMS收集器配合工作。Parallel Scavenge收集器也是一个新生代收集器使用复制算法也是并行的多线程收集器。它的特点是它的关注点与其他收集器不同。CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。 吞吐量 运行用户代码时间 / (运行用户代码时间 垃圾收集时间)。虚拟机总共运行了100分钟其中垃圾收集花掉1分钟那吞吐量就是99%。优点可以设置最大垃圾收集停顿时间(-XX:MaxGCPauseMillis)和直接设置吞吐量大小(-XX:GCTimeRatio)。还有一个-XX:UseAdaptiveSizePolicy参数这是一个开关参数打开之后就不需要手动指定新生代大小、Eden与Survivor区的比例等细节参数了虚拟机会根据当前系统的运行情况收集性能监控信息动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。这种调节方式称为GC自适应的调节策略。缺点与CMS不兼容。适用场景适合在后台运算而不需要太多交互的任务例如批量处理、订单处理、工资支付、科学计算等主要目标是高效率地利用CPU时间尽快完成程序的运算任务。3.2 老年代收集器应对高存活率对象Serial Old收集器是Serial收集器的老年代版本同样是一个单线程收集器使用“标记-整理”算法。用途主要意义也是供Client模式下的虚拟机使用。在Server模式下它主要有两大用途一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用另一种用途就是作为CMS收集器发生失败时的后备预案。Parallel Old收集器是Parallel Scavenge收集器的老年代版本使用多线程和“标记-整理”算法。这个收集器是在JDK 1.6中才开始提供的。优点在注重吞吐量以及CPU资源敏感的场合可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合。在此之前Parallel Scavenge只能搭配Serial Old而Serial Old在服务端性能上的“拖累”使得Parallel Scavenge的吞吐量优势无法充分发挥。适用场景与Parallel Scavenge搭配组成“吞吐量优先”的黄金组合。CMS收集器这是一种以获取最短回收停顿时间为目标的收集器。它非常符合互联网站或者B/S系统的服务端应用这类应用尤其重视服务的响应速度希望系统停顿时间最短。工作过程基于“标记-清除”算法整个过程分为4个步骤初始标记仅仅标记一下GC Roots能直接关联到的对象速度很快需要STW。并发标记进行GC Roots Tracing的过程从初始标记的对象开始遍历整个对象图这个过程耗时较长但可以与用户线程并发执行。重新标记修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。这个阶段的停顿时间通常会比初始标记阶段稍长但远比并发标记时间短需要STW。并发清除清理删除掉标记阶段判断的已经死亡的对象。由于不需要移动存活对象这个阶段也可以与用户线程并发执行。优点并发收集低停顿。缺点对CPU资源非常敏感在并发阶段它虽然不会导致用户线程停顿但会因为占用了一部分线程或者说CPU资源而导致应用程序变慢总吞吐量会降低。CMS默认启动的回收线程数是CPU核心数3/4。无法处理“浮动垃圾”在并发清除阶段用户线程还在运行自然就会有新的垃圾产生。这部分垃圾出现在标记过程之后CMS无法在当次收集中处理掉它们只好留到下一次GC时再清理。这部分垃圾就称为“浮动垃圾”。因此CMS不能像其他收集器那样等到老年代几乎完全被填满了再进行收集必须预留一部分空间供并发收集时的程序运作使用。可以通过-XX:CMSInitiatingOccupancyFraction参数来设置触发百分比。如果预留空间无法满足程序需要就会出现一次“并发失败”这时虚拟机将启动后备预案临时启用Serial Old收集器来重新进行老年代的垃圾收集这样停顿时间就很长了。会产生空间碎片由于基于“标记-清除”算法收集结束时会产生大量不连续的空间碎片。空间碎片过多时会给大对象分配带来麻烦往往会出现老年代还有很大空间剩余但是无法找到足够大的连续空间来分配当前对象而不得不提前触发一次Full GC。为了解决这个问题CMS收集器提供了一个-XX:UseCMSCompactAtFullCollection开关参数默认开启用于在Full GC时开启内存碎片的合并整理过程但这个整理过程是无法并发的空间碎片问题没有了但停顿时间又会变长。3.3 面向全堆的革新者G1收集器G1收集器是垃圾收集器技术发展历史上的一个里程碑式的成果它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。在JDK 9中G1被设置为默认的垃圾收集器。核心设计思想G1不再坚持固定大小以及固定数量的分代区域划分而是把连续的Java堆划分为多个大小相等的独立区域Region每一个Region都可以根据需要扮演新生代的Eden空间、Survivor空间或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理。Region中还有一类特殊的Humongous区域专门用来存储大对象大小超过Region容量一半的对象。G1的运作过程可以划分为四个阶段虽然也保留了新生代和老年代的概念但二者不再是物理隔离而是一系列Region不需要连续的集合。初始标记仅仅标记一下GC Roots能直接关联到的对象并且修改TAMS指针的值让下一阶段用户线程并发运行时能正确地在可用的Region中分配新对象。这个阶段需要停顿线程但耗时很短。并发标记从GC Root开始对堆中对象进行可达性分析递归扫描整个堆里的对象图找出要回收的对象。这阶段耗时较长但可与用户程序并发执行。最终标记对用户线程做另一个短暂的暂停用于处理并发阶段结束后仍遗留下来的少量SATB原始快照记录。筛选回收首先对各个Region的回收价值和成本进行排序根据用户所期望的停顿时间来制定回收计划。然后将决定回收的Region中的存活对象复制到空的Region中再清理掉整个旧的Region。这里涉及到存活对象的移动必须暂停用户线程由多条收集线程并行完成。G1的优势可预测的停顿时间模型这是G1相对于CMS的一个巨大优势。G1跟踪各个Region里面的垃圾堆积的“价值”大小回收所获得的空间大小以及回收所需时间的经验值在后台维护一个优先列表每次根据允许的收集时间优先回收价值最大的Region。这种使用Region划分内存空间以及有优先级的区域回收方式保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。空间整合从整体上看G1是基于“标记-整理”算法实现的收集器从局部两个Region之间上看又是基于“复制”算法实现的。这意味着G1运作期间不会产生内存空间碎片收集后能提供规整的可用内存。更精细的调优用户可以通过参数-XX:MaxGCPauseMillis指定一个期望的停顿时间目标默认200ms。G1会尽力达成这个目标但并非绝对保证。G1的适用场景与调优G1的设计目标是替代CMS适用于服务端、大内存、多核处理器的应用。它尤其适合那些需要低延迟且堆内存较大的场景。调优G1时核心是设定合理的停顿时间目标-XX:MaxGCPauseMillis并给予足够的堆内存。通常不需要像调优Parallel或CMS那样手动设置新生代、老年代的大小比例。4. 新一代低延迟收集器ZGC与Shenandoah随着硬件发展和大内存应用如数十GB甚至上百GB堆内存的普及用户对垃圾收集的停顿时间延迟要求越来越苛刻。G1虽然比CMS有了很大进步但在超大堆下其停顿时间特别是Full GC仍然可能达到秒级。为此Oracle的ZGC和Red Hat的Shenandoah应运而生它们的目标都是在超大堆内存TB级别下将停顿时间控制在10毫秒以内。4.1 ZGC基于染色指针的并发奇迹ZGCZ Garbage Collector是Oracle在JDK 11中引入的实验性功能并在JDK 15中正式发布。它的设计目标是实现低延迟亚毫秒到几毫秒的同时处理从几百MB到数TB的堆大小。核心技术染色指针这是ZGC最核心的黑科技。它将少量额外的信息称为“元数据”存储在对象指针本身中而不是像传统收集器那样存储在对象头或独立的数据结构中。在64位系统中ZGC利用了虚拟地址空间的巨大空间将指针的高位目前是42-45位用来存储标记信息如标记、重映射、已移动等状态。优点大幅减少内存屏障由于状态信息在指针上访问对象时可以直接判断减少了对内存屏障的依赖提升了并发效率。高效的并发转移对象转移即从旧地址复制到新地址后只需要修改指向它的指针中的地址部分而所有持有该对象旧指针的线程在后续访问时可以通过指针中的“已移动”标记触发一个“自愈”操作自动将指针修正为新地址。这个过程是并发的且非常快速。区域划分灵活ZGC将堆划分为许多小页面类似于G1的Region但大小不固定并且没有分代概念在JDK 21中引入了分代式ZGC即ZGenerational。工作阶段ZGC的收集周期也分为标记、转移相当于清理/整理等阶段但所有这些阶段几乎都是并发执行的。其停顿时间几乎只与GC Roots的数量有关而与堆中存活对象的总大小无关。因此即使堆非常大停顿时间也能保持在一个极低的水平。使用与调优启用ZGC非常简单使用JVM参数-XX:UseZGC即可。对于超大堆通常还需要设置-Xmx,-Xms。ZGC的调优参数相对较少核心是设置最大停顿时间目标-XX:MaxGCPauseMillis默认无限制和并发GC线程数-XX:ConcGCThreads。对于大多数应用使用默认参数就能获得很好的效果。4.2 Shenandoah低延迟的另一个选择Shenandoah是由Red Hat领导开发的一款低停顿时间垃圾收集器最初在OpenJDK 12中作为实验性功能引入。它的目标与ZGC类似在超大堆上实现低延迟。核心技术Brooks指针Shenandoah实现并发转移的核心技术是“Brooks指针”。它在每个对象前面增加了一个额外的引用字段forwarding pointer。当对象被移动时会在原对象位置留下一个“转发指针”指向对象的新地址。工作过程当并发转移发生时收集器将对象复制到新位置并在旧位置设置转发指针。应用程序线程在访问对象时会先检查这个转发指针。如果存在则通过它“转发”到新地址进行访问。这个检查-转发操作通过读屏障实现。与ZGC的区别虽然目标一致但实现路径不同。ZGC使用染色指针将元数据嵌入指针Shenandoah使用Brooks指针将元数据存储在对象旁边。Shenandoah的读屏障开销在早期版本中相对较高但经过持续优化其性能已经非常出色。适用场景对比ZGC由Oracle官方大力支持是未来JDK发展的重点。其染色指针设计非常精妙在支持它的平台上主要是Linux/AArch64和x86-64性能表现卓越。如果追求最前沿的技术和官方长期支持ZGC是首选。Shenandoah由Red Hat社区驱动其优势在于更广泛的平台支持包括一些ZGC尚未优化的平台和更早的可用性。在一些特定工作负载下其性能可能与ZGC互有胜负。实操心得如何选择新一代收集器JDK版本首先确认你的JDK版本。ZGC在JDK 15后正式生产可用Shenandoah在JDK 12后作为实验性功能在后续版本中逐步稳定。建议使用最新的LTS版本如JDK 17, 21以获得最佳支持和性能。堆大小与延迟要求如果你的应用堆内存小于8GB对延迟不敏感停顿几百毫秒可接受那么G1甚至Parallel Scavenge/Old可能就足够了它们更成熟稳定。如果堆内存大于16GB且要求停顿时间在100毫秒甚至10毫秒以下那么ZGC或Shenandoah是必然选择。平台与供应商检查你的运行环境。ZGC目前对Linux和某些BSD支持最好。Shenandoah的平台支持略广一些。如果你使用的是某个特定的JDK发行版如Red Hat的OpenJDK它可能对Shenandoah有更好的集成和优化。测试测试测试这是最重要的原则。在生产环境全面切换前务必使用与生产环境硬件、数据量、流量模式相似的测试环境进行充分的压力测试和基准测试。观察关键指标应用吞吐量、P99/P999延迟、GC停顿时间、CPU使用率等。没有一种收集器是万能的最适合的才是最好的。5. 实战GC日志分析与性能调优指南理论懂了收集器也认识了最终还是要落到实战上。如何监控GC状态如何从GC日志中发现问题又该如何调优这是每个Java开发者必须掌握的技能。5.1 读懂GC日志你的应用健康晴雨表开启GC日志是分析问题的第一步。常用的JVM参数如下-XX:PrintGCDetails // 打印详细的GC日志 -XX:PrintGCDateStamps // 打印GC发生的时间戳 -XX:PrintGCTimeStamps // 打印GC事件相对于JVM启动的时间戳 -Xloggc:/path/to/gc.log // 将GC日志输出到文件 // JDK 9 统一日志框架推荐用法 -Xlog:gc*,gcagetrace,safepoint:filegc.log:time,uptime,level,tags:filecount10,filesize10m我们来看一段典型的Parallel Scavenge/Old收集器的GC日志2024-05-27T10:00:00.1230800: 1.234: [GC (Allocation Failure) [PSYoungGen: 65536K-8192K(76288K)] 65536K-24576K(251392K), 0.0123456 secs] [Times: user0.03 sys0.00, real0.01 secs] 2024-05-27T10:00:10.4560800: 11.567: [Full GC (Ergonomics) [PSYoungGen: 8192K-0K(76288K)] [ParOldGen: 163840K-150000K(175104K)] 172032K-150000K(251392K), [Metaspace: 3456K-3456K(1056768K)], 0.456789 secs] [Times: user1.23 sys0.01, real0.46 secs]日志解析2024-05-27T10:00:00.1230800GC发生的绝对时间。1.234GC事件距离JVM启动的时间秒。[GC或[Full GCGC的类型Full GC意味着整个堆包括新生代、老年代、元空间等都被回收通常停顿时间很长。(Allocation Failure)触发GC的原因。这里是“分配失败”即新生代Eden区空间不足无法为新对象分配内存。[PSYoungGen: 65536K-8192K(76288K)]新生代Parallel Scavenge的回收情况。回收前使用了65536K回收后剩余8192K该区域总容量为76288K。65536K-24576K(251392K)整个堆的回收情况。回收前堆使用量65536K回收后24576K当前堆总容量251392K。0.0123456 secs本次GC所花费的时间秒。[Times: user0.03 sys0.00, real0.01 secs]CPU时间与墙钟时间。user是垃圾收集器线程消耗的所有CPU时间sys是系统调用消耗的时间real是操作从开始到结束经历的墙钟时间。如果是多核并行收集user时间可能远大于real时间。需要警惕的GC日志信号频繁的Full GC这是最危险的信号。如果日志中频繁出现[Full GC且每次耗时都很长1秒说明应用可能存在内存泄漏、堆大小设置不合理、或者对象晋升过快等问题。GC后内存回收效果差看箭头前后的数字。例如老年代在Full GC后175104K-170000K(175104K)意味着回收前用了170G回收后还有169.9G几乎没回收掉什么对象。这强烈暗示存在内存泄漏大量对象被长期持有。晋升失败Promotion Failure在Parallel Scavenge日志中可能出现。当新生代存活对象需要晋升到老年代但老年代空间不足时发生会触发一次Full GC。这通常说明老年代空间设置太小或者存在“朝生夕死”的大对象直接进入了老年代。并发模式失败Concurrent Mode Failure在CMS收集器日志中常见。这意味着CMS在并发清理过程中老年代空间被快速填满不得不退化为Serial Old进行Full GC导致长时间停顿。需要调整-XX:CMSInitiatingOccupancyFraction参数让CMS更早启动。5.2 性能调优实战从问题现象到解决方案调优没有银弹必须结合监控指标和日志分析。下面是一个典型的调优思路流程场景一应用响应时间偶尔出现尖刺毛刺现象应用P99延迟正常但P999延迟或最大响应时间偶尔会飙升至数秒。通过系统监控发现响应时间尖刺与CPU使用率高峰吻合。分析查看对应时间点的GC日志。很可能发现了一次长时间的Full GC停顿。例如日志显示一次Full GC耗时2.5秒。排查检查堆内存大小使用jstat -gcutil pid 1000命令实时观察各区域使用率。如果老年代使用率经常接近100%说明堆可能太小。适当增加-Xmx和-Xms建议设置成相等避免堆动态调整带来的开销。检查对象晋升观察jstat输出中YGCYoung GC次数和FGCFull GC次数的比值。如果YGC很频繁但FGC也不少说明很多对象“过早晋升”到了老年代。可以尝试调整新生代大小-Xmn或者调整晋升年龄阈值-XX:MaxTenuringThreshold默认15。检查大对象如果应用有大量大对象如大数组、大字符串它们会直接进入老年代或G1的Humongous区。检查代码逻辑看是否能优化大对象的使用比如使用流式处理代替全量加载。切换低延迟收集器如果堆内存较大16GB且Full GC无法避免考虑从Parallel或CMS切换到G1甚至ZGC/Shenandoah。对于CMS可以尝试优化-XX:CMSInitiatingOccupancyFraction如从68%调到60%并确保有足够的CPU资源。场景二应用吞吐量不达标现象CPU使用率很高但系统处理事务的TPS每秒事务数低于预期。分析GC本身也是CPU密集型操作。如果GC线程占用了过多CPU时间留给业务线程的时间就少了。使用jstat -gcutil pid 1000观察GCTGC总时间占YGC/FGC周期的比例。也可以使用top -Hp pid查看GC线程的CPU消耗。排查减少GC频率如果YGC非常频繁每秒几次说明新生代太小对象很快占满Eden区。适当增大新生代-Xmn但注意不要太大否则单次YGC时间会变长。目标是找到一个平衡点。调整GC线程数对于Parallel收集器可以通过-XX:ParallelGCThreads设置并行GC线程数。默认值通常与CPU核心数相关。如果GC线程数过多会加剧CPU竞争过少则拉长GC时间。需要根据实际CPU核心数和应用负载调整。关注对象分配速率使用jstat -gc pid 1000观察EUEden区使用量的增长速度。如果分配速率极高可能是代码中存在大量不必要的临时对象创建如在循环中创建对象、大量使用String拼接等。这时需要优化代码减少对象分配。场景三使用G1时停顿时间超过预期现象为G1设置了-XX:MaxGCPauseMillis200但监控显示偶尔停顿超过500ms。分析G1的停顿时间目标只是一个软目标。超过目标可能原因有1. 混合收集Mixed GC需要处理的Region太多2. 大对象Humongous对象分配或回收的影响3. 并发标记周期耗时过长。排查分析GC日志启用更详细的G1日志-XX:PrintAdaptiveSizePolicy和-Xlog:gcergo*trace。查看是哪个阶段如Evacuation Pause, Concurrent Mark耗时过长。调整Region大小G1的Region大小由堆大小自动决定1M-32M。如果存在大量中等大小的对象可以尝试通过-XX:G1HeapRegionSize手动设置Region大小使其更匹配对象大小分布。关注Humongous对象使用jcmd pid GC.humongous_info查看大对象信息。如果大对象过多且生命周期短会对G1性能造成压力。考虑优化代码避免分配短命的大对象。调整并发线程数通过-XX:ConcGCThreads控制并发标记阶段的线程数。增加此值可以加快并发标记但会占用更多应用线程的CPU。避坑技巧一份GC参数设置清单基本参数-Xms和-Xmx务必设置成相同值避免堆动态扩容收缩带来的性能损耗。-XX:HeapDumpOnOutOfMemoryError和-XX:HeapDumpPath内存溢出时自动转储堆快照是事后分析的救命稻草。日志参数JDK 8及之前-XX:PrintGCDetails -XX:PrintGCDateStamps -Xloggc:/path/to/gc.log日志参数JDK 9-Xlog:gc*,gcagetrace,safepoint:filegc.log:time,uptime,level,tags:filecount10,filesize10mParallel Scavenge/Old调优-XX:MaxGCPauseMillis100设置期望的最大GC停顿时间毫秒。收集器会尽力达成但不保证。-XX:GCTimeRatio99设置吞吐量目标。公式1 / (1 GCTimeRatio)。这里是99表示GC时间不超过总时间的1%。-XX:UseAdaptiveSizePolicy启用自适应策略默认开启让JVM自动调整各区大小。G1调优-XX:MaxGCPauseMillis200核心目标参数。-XX:InitiatingHeapOccupancyPercent45触发并发标记周期的堆占用率阈值。如果老年代增长快可以调低如35。-XX:G1ReservePercent10堆内存的保留比例用于晋升失败时的回退空间。如果频繁发生晋升失败可以适当增加。通用建议不要过度调优JVM的默认参数和自适应机制对于大多数应用已经足够好。在遇到明确的性能问题之前不要盲目添加大量GC参数。一次只改变一个参数调优时记录基准性能然后一次只调整一个参数并观察效果这样才能准确定位每个参数的影响。监控与日志是关键没有监控和日志调优就是盲人摸象。务必建立完善的APM应用性能监控和日志收集体系。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价