垃圾回收策略与调优(GC Tuning) — 通用指南!

举报
喵手 发表于 2026/01/15 17:51:55 2026/01/15
【摘要】 开篇语哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛  今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。  我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,...

开篇语

哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛

  今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。

  我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。

小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持!

1. GC 基本算法(原理速览)

理解三种经典回收策略有助于选择和调优:

  • 标记-清除(Mark–Sweep)
    原理:标记所有可达对象 → 清除未标记对象。优点:空间利用率高;缺点:产生内存碎片,可能导致长期停顿(stop-the-world)在清理阶段。适合老年代回收的思路。

  • 复制(Copying)
    原理:把存活对象从当前区复制到另一块连续内存(通常用于年轻代),然后清空原区。优点:分配快、没有碎片;缺点:需要双倍空间(或半区策略),不适合大对象/老年代。

  • 标记-整理(Mark–Compact / Mark–Sweep–Compact)
    原理:标记可达对象,然后把存活对象移动压紧以消除碎片(整理)。优点:避免碎片;缺点:移动成本(需要额外时间),可能产生停顿。

现代 JVM 的 GC 都是这些策略的组合(年轻代常用复制,老年代常用标记-整理或并发整理),不同 GC 的差别在于并发与并行执行、暂停控制、对大堆的扩展性等。

2. 常见 GC(对比与适用场景)

(当前主流 JVM 实现:Serial、Parallel (Throughput)、CMS(旧)、G1、ZGC、Shenandoah)

  • Serial GC(-XX:+UseSerialGC)
    单线程回收,适合小堆、单核或开发/嵌入式环境。优点简单、开销小;缺点单线程暂停。

  • Parallel GC(又称吞吐量优先,-XX:+UseParallelGC)
    并行执行年轻代/老年代回收,用于多核且对吞吐量敏感的批处理程序。调参项多(ParallelGCThreads、MaxGCPauseMillis)。

  • G1 GC(-XX:+UseG1GC)
    默认在很多较新 JDK(从 Java 9/11 起)中常用:分区(region)设计、以低停顿为目标,混合并发/并行回收。适合大堆、想平衡延迟与吞吐的服务。重要参数:-XX:MaxGCPauseMillis-XX:InitiatingHeapOccupancyPercent-XX:ConcGCThreads 等。

  • ZGC(-XX:+UseZGC)
    设计目标:超低停顿(毫秒级或亚毫秒),并能支持超大堆(TB 级)。实现大量并发线程和色标指针算法(depends on JVM version)。适合对延迟极端敏感且堆较大的应用。可配置项较少(很多自动)。

  • Shenandoah(-XX:+UseShenandoahGC)
    类似 ZGC,目标低停顿,Red Hat / Oracle OpenJDK 支持。对某些工作负载表现优异,但实现细节与 ZGC 有差异。

选择原则(简单总结):

  • 小堆、单线程或工具/测试:Serial。
  • 吞吐优先(后台批量):Parallel。
  • 平衡延迟与吞吐,且堆中等~大:G1 是常见默认选择。
  • 极低停顿(在线交易/实时服务)且可用(JDK 支持):ZGC 或 Shenandoah。

3. 年轻代 / 年老代 / 晋升策略

  • 年轻代(Young):新对象分配在 Eden;通过复制(Minor GC)回收,把存活对象复制到 Survivor 区,存活次数超过阈值/年龄的对象晋升(promote)到老年代。

  • 年老代(Old/Tenured):长期存活对象,采用标记-整理或并发整理策略(取决于 GC)。Major / Full GC 发生时会回收老年代,通常代价更高。

  • 晋升策略注意点

    • 频繁 Minor GC 并伴随大量晋升会触发老年代快速增长(promotion failure -> Full GC)。
    • 控制年轻代大小和晋升年龄(如 G1 的 -XX:MaxTenuringThreshold)能影响晋升频度。
    • 大对象(large arrays、big ByteBuffer)通常直接进入老年代或被特殊处理(取决 JVM),会影响老年代压力。

4. 关键 JVM 参数与日志(实用清单)

基本堆参数

  • -Xms<size>:JVM 初始堆大小。
  • -Xmx<size>:JVM 最大堆大小。通常建议把 -Xms-Xmx 设为相同以避免在运行时扩缩容(可减少产生日志/碎片),但这会占用固定内存。
  • -XX:MaxMetaspaceSize=<size>:类元数据区(Metaspace)上限(Java 8+)。
  • -XX:ReservedCodeCacheSize:JIT 代码缓存。

GC 选择与调参(示例)

  • -XX:+UseG1GC:启用 G1。常见参数:

    • -XX:MaxGCPauseMillis=200(目标最大停顿,非保证)
    • -XX:InitiatingHeapOccupancyPercent=45(触发混合回收的占用阈值)
  • -XX:+UseZGC:启用 ZGC(根据 JDK 版本可用)。

  • -XX:+UseShenandoahGC:启用 Shenandoah(OpenJDK builds)。

  • 并行/并发线程:-XX:ParallelGCThreads=N-XX:ConcGCThreads=N

GC 日志(建议)

现代 JDK 使用统一日志系统:

  • -Xlog:gc*:file=gc.log:time,level,tags(JDK 9+ 风格)
    如果是老版本(Java 8):
  • -Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime

日志必备信息用于分析:每次 GC 的时间戳、GC 类型(Young/Mixed/Full)、停顿时长(stop-the-world)、回收前后堆使用情况、总堆大小、GC 触发原因(Allocation Failure / System.gc / Metadata GC 等)。

5. GC 日志解析要点(如何看懂常见字段)

解析 GC 日志的目标:定位停顿(pause)来源、频度、回收效率与晋升行为。常见分析步骤:

  1. 找到频繁的短暂停顿还是少量长暂停?

    • 频繁短暂停顿通常受 Minor GC 驱动(年轻代)或频繁分配/晋升。
    • 少量长暂停多与 Full GC 或并发阶段失败/停顿有关。
  2. 看回收前后的堆占用(used → used_after)与回收量(reclaimed):

    • 如果每次 GC 后堆仍然很满,说明老年代正被压满,需要扩大堆或降低对象存活率/晋升。
    • 如果 Minor GC 回收很少但频繁发生,可能对象寿命稍长导致频繁晋升或 eden 太小。
  3. 关注 Promotion Failure / Concurrent Mode Failure(G1/CMS):

    • Promotion Failure:晋升到老年代失败,触发 Full GC。原因通常是老年代空间不足或晋升速率过快。
    • Concurrent Mode Failure(CMS):并发回收未能及时完成,退化为 Stop-the-world Full GC。
  4. 对比应用侧指标(响应时间、吞吐量、应用日志)与 GC 日志时间线,找出“GC 停顿与请求延迟”之间的对应关系。

  5. 使用工具辅助可视化(GCViewer、GCEasy、Grafana + Prometheus 收集 jstat/jcmd 指标、Java Flight Recorder)。

6. 常用诊断工具与命令(现场排查利器)

  • jstat -gcutil <pid> <interval> <count>:实时查看堆/GC 某些指标。
  • jmap -histo:live <pid>:查看对象分布直方图(有助定位大量占用实例)。
  • jcmd <pid> GC.heap_infojcmd <pid> GC.class_histogramjcmd <pid> VM.native_memory summary(Native Memory Tracking 必须启动)。
  • jcmd <pid> GC.run:强制触发 GC(谨慎)。
  • jstack <pid>:线程堆栈,用于排查卡顿原因是否因 GC 以外的阻塞。
  • Java Flight Recorder(JFR):内建的轻量级性能采样,能记录 GC / allocation / locks / jfr events。
  • VisualVM、YourKit、Async-profiler:内存、CPU、热点分析。
  • GC 日志可视化:GCViewer / GCeasy / internal parsing scripts。

重要:排查 native / off-heap 内存(DirectByteBuffer、JNI、本地库)时使用 jcmd <pid> VM.native_memory summary 或启用 NMT(-XX:NativeMemoryTracking=summary)来查原生内存使用。

7. 常见调优场景与步骤(实战流程)

通用调优流程(工程上可复用):

  1. 定义目标与约束:明确 SLO(p99 延迟 < X ms)或吞吐目标及内存/成本边界。
  2. 收集基线数据:采集 GC 日志、应用请求延迟分布、CPU/内存使用、对象分配速率。
  3. 定位瓶颈:是年轻代频繁 GC?还是老年代触发 Full GC?还是堆外(native)内存耗尽?
  4. 小步尝试与验证:调整参数单一变量(如扩大年轻代或增大堆)并运行负载测试(JMH 或真实流量下的回放),观察指标。
  5. 回退与自动化:保持变更可回退并记录每次试验的 GC 日志与性能差异。
  6. 长期监控与报警:设置 GC 健康阈值(Full GC 频次、平均停顿时间、OldGen 使用率)并告警。

典型调优示例(思路,不是万金油):

  • 场景 A(吞吐需要提高):考虑 -XX:+UseParallelGC、增加 -Xmx、调大 ParallelGCThreads
  • 场景 B(p99 延迟太高):考虑 -XX:+UseG1GC(或 ZGC / Shenandoah),设定 -XX:MaxGCPauseMillis 小值,调整 InitiatingHeapOccupancyPercent 以提前触发并发回收。
  • 场景 C(频繁 Full GC):检查老年代增长速率、减少晋升速率(增加 Young 大小或提高 MaxTenuringThreshold),或扩大堆。
  • 场景 D(堆外内存爆满):启用 NMT,检查 DirectByteBuffer 使用、Netty off-heap pool、JNI。本质上要追踪 native/OS 层的分配。

8. 内存泄露排查方法

常用步骤与技巧:

  1. jmap -histo / jcmd GC.class_histogram:看类实例数量和占用比例。
  2. 堆转储(heap dump)分析jcmd <pid> GC.heap_dump /tmp/heap.hprof,用 MAT(Eclipse Memory Analyzer)分析 dominator tree、largest retained sizes、leak suspects。
  3. 观察堆增长曲线:若在没有请求量增长的情况下堆持续上升,说明可能有泄露。结合对象保留链(who retains)定位泄露 root。
  4. 检查静态集合、缓存、线程本地变量(ThreadLocal):常见泄露源。
  5. 检查第三方库、native 内存:有时 leak 不是 JVM 堆,而是 native(DirectByteBuffer、本地缓存、JNI),需 NMT 或 OS 层工具(top, pmap, smem)来分析。

9. 实战练习(实验计划:测延迟与吞吐并分析 GC 日志)

下面给出可重复的实验步骤(你可以用本地机器或测试环境跑):

准备

  • 使用一个可控负载生成器(例如:wrk、httperf,或用 JMH 写 microbench)。建议使用 JMH 来测试 Java 内存/分配相关场景。
  • 准备一个代表性应用(例如:Spring Boot 简单 REST 服务,或自写的分配/短命对象/长寿命对象混合的测试程序)。

实验 A:不同 GC 的对比

  1. 固定负载(例如 1000 RPS 或某分配速率),在相同机器上分别运行:

    • java -Xms4g -Xmx4g -XX:+UseG1GC -Xlog:gc*:file=g1.log:time,level 应用.jar
    • java -Xms4g -Xmx4g -XX:+UseZGC -Xlog:gc*:file=zgc.log:time,level 应用.jar
    • java -Xms4g -Xmx4g -XX:+UseParallelGC -Xlog:gc*:file=par.log:time,level 应用.jar
  2. 记录指标:p50/p95/p99 响应时间、吞吐、GC 暂停分布(从 gc.log)。

  3. 分析:比较不同 GC 在相同负载下的停顿分布和吞吐差异,找出最佳选择。

实验 B:调优 G1(示例)

  1. 在 G1 下逐步调整:-XX:MaxGCPauseMillis=20010050,观察停顿与 CPU 使用变化。通常降低目标停顿会换来更高的并发/CPU。
  2. 调整 -XX:InitiatingHeapOccupancyPercent=30/45/60,观察 Mixed GC 触发频率与 Full GC 发生概率。较小值会更早启动并发回收,降低触发 Full GC 风险,但会占用更多并发资源。
  3. 记录并绘图(停顿时间分布 vs CPU 使用 vs 吞吐),择优配置。

实验 C:原型注入问题(prototype vs singleton 示例)

  • 在单例服务中注入 prototype,并使用 ObjectProvider / @Lookup / Provider,观察每次请求是否获得新的实例,验证正确行为并测量分配压力。

分析工具建议:用 GC 可视化工具(GCViewer / GCeasy 或自家脚本),并把 GC 日志与应用请求日志时间轴对齐。

10. 常见陷阱(不要犯的错)

  • 盲目增大堆(“加大 Xmx 就能解决一切”):堆变大能减缓 Full GC 发生频率,但会增加每次停顿(尤其对非并发回收)和内存成本。且掩盖内存泄露问题。
  • 忽略堆外内存(DirectByteBuffer / JNI):JVM 堆看起来正常但进程仍 OOM,是 native 内存耗尽的典型表现。必须同时检查 NMT/OS 层。
  • 混乱的监控与告警:没有基线与告警阈值会导致无法及时察觉 GC 恶化。
  • 在生产直接改大量 GC 参数而未经回放测试:小步验证,不同机器/负载差异可能很大。
  • 使用不再受支持或已弃用的 GC(例如 CMS 在较新 JDK 被标注为 deprecated/removed):确保你的 JDK 版本对目标 GC 的支持并查阅官方变更日志。
  • 在构造/初始化中做大量 IO/网络调用:这些会和 GC/启动交互复杂,建议延迟初始化或异步初始化。

11. 延伸阅读(建议按需阅读)

  • 官方 JDK 文档(GC/Tuning 指南) — 查看你使用的 JDK 版本对应的 GC 文档(Java 8/11/17/21 文档在细节上会不同)。
  • 各 GC 的白皮书 / 设计论文(G1、ZGC、Shenandoah)可深入理解实现原理。
  • 常用工具文档(jcmd、jmap、jstat、Java Flight Recorder)。

… …

文末

好啦,以上就是我这期的全部内容,如果有任何疑问,欢迎下方留言哦,咱们下期见。

… …

学习不分先后,知识不分多少;事无巨细,当以虚心求教;三人行,必有我师焉!!!

wished for you successed !!!


⭐️若喜欢我,就请关注我叭。

⭐️若对您有用,就请点赞叭。
⭐️若有疑问,就请评论留言告诉我叭。


版权声明:本文由作者原创,转载请注明出处,谢谢支持!

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。