一、GC 的基本原理(所有收集器的共同基础)

  • 判活方式(Reachability):从一组根(GC Roots)出发可达的对象视为存活。GC Roots 包括:栈上的局部变量、JNI 引用、运行中的线程、类的静态字段、被锁的对象等。
  • 安全点(Safepoint):需要停顿世界(STW)的阶段,所有线程在“可中断位置”停下来,便于做堆的一致性处理(如初始标记、对象移动等)。
  • 引用强度:强引用(Strong)不回收;软引用(Soft)在内存吃紧时回收;弱引用(Weak)在下一次 GC 就回收;虚引用(Phantom)仅用于回收后通知。
  • 写屏障与记忆集:为支持“分代”并发/增量标记,需要记录跨代指针(如老年代指向年轻代)。实现上常用Card Table + 写屏障维护“脏卡”。
  • 分代假设:大多数对象“朝生夕死”,少数对象活得很久——因此分为年轻代(Eden + Survivor)和老年代以优化回收。

对判活方式、安全点、引用强度、写屏障与记忆集不了解的朋友,可以直接点进去看我之前的文章

二、经典算法

  1. 标记-清除(Mark-Sweep)
    • 步骤:标记活对象 → 清除未标记对象。
    • 优点:实现简单,吞吐高。
    • 缺点:产生碎片,可能导致大对象分配失败(进而触发 Full GC)。
  2. 标记-整理(Mark-Compact)
    • 步骤:标记活对象 → 把活对象往一端“挤压”(移动/整理)→ 清理边界之外的空间。
    • 优点无碎片,适合老年代。
    • 缺点:需要移动对象,停顿相对长(除非能并发/增量化)。
  3. 复制(Copying / Scavenge)
    • 把堆分为from-spaceto-space(两块等大小或按策略划分)。
    • 一次 GC 的正确方向from 里的存活对象复制到 to,然后清空 from下一次互换角色(swap)。
    • 优点:无碎片、对象分配快(Bump-the-pointer)。
    • 缺点需要预留一块 to-space,看似“只用到一半内存”;但在年轻代中,大量对象会被直接回收,复制成本相对低。
  4. 分代(Generational)
    • 严格说是策略而不是算法。年轻代常用复制算法(Eden → Survivor 互拷贝);老年代常用标记-整理带并发的区域化整理
    • 年轻代回收称 Minor/Young GC,老年代回收称 Major/Old GCFull GC 是整堆+元数据等的全停顿回收。

三、HotSpot 常见收集器(理解“算法如何落地”)

1) 早期/基础型

  • Serial:单线程,年轻代复制 + 老年代标记整理,全部 STW。简单但停顿长;适合单核或小堆。
  • Parallel(又称 Throughput):多线程并行的 Serial,追求吞吐;仍是 STW,停顿比 Serial 短但仍可能明显;适合批处理、高吞吐。

2) CMS(Concurrent Mark-Sweep,已过时/移除)

  • 老年代并发标记 + 并发清除不压缩,因此易碎片化浮动垃圾,最终可能退化为 Full GC。已被 G1/ZGC/Shenandoah 取代。

3) G1(Garbage-First)区域化 + 并发标记 + 按需整理

  • 把堆切成很多Region(年轻/老年代都是 region 的逻辑集合),能按“收益优先”选择回收集(Collection Set, CSet)。
  • 年轻代采用复制/疏散(evacuation)老年代也可通过区域化疏散达到“渐进式压缩”,减少碎片。
  • 并发标记采用 SATB(Snapshot-At-The-Beginning)三色标记法,停顿可控;支持暂停时间目标(如 -XX:MaxGCPauseMillis)。
  • 适合中大堆,通用默认选择。

4) ZGC超低停顿(毫秒级),并发重定位

  • 通过着色指针(Colored Pointers)和加载屏障(Load Barriers)实现几乎全流程并发:标记、重定位都在并发进行,STW 只在极短的阶段(如初始标记)。
  • 支持巨大堆,停顿时间与堆大小基本解耦
  • 现代 JDK 已提供分代 ZGC(Generational ZGC),进一步降低写屏障/记忆集成本并提升吞吐。

5) Shenandoah(Red Hat 主导)

  • 也是并发疏散的低停顿收集器,使用Brooks Pointer(转发指针)与读/写屏障。目标与 ZGC 相似,细节不同。

实战建议(通用):

  • 小堆/简单场景:Parallel;
  • 中大堆 & 综合平衡:G1(默认);
  • 对延迟极敏感:ZGC / Shenandoah(优先 ZGC)。

四、关键细节(面试/排障高频)

  • 年轻代布局Eden + Survivor0(S0) + Survivor1(S1)
    • 分配主要在 Eden(配合 TLAB 提升线程本地分配速度)。
    • Minor GC 时:把 Eden + (其中一个 Survivor,称 from) 中的存活对象复制另一个 Survivor(to)复制方向是 from → to,并清空 Eden 与 fromS0/S1 角色互换
    • 晋升(Promotion):对象在 Survivor 里“熬”过若干次 GC(由 XX:MaxTenuringThreshold 控制,JIT 会自适应)就会晋升老年代;大对象也可能直接晋升或走专门通道(如 G1 的 Humongous Allocation)。
  • 老年代回收触发:当老年代空间不足或达到阈值(如 G1 的 IHOP 触发并发标记)→ 进入并发/混合回收;若晋升失败/疏散失败可能退化为 Full GC
  • 三色标记 & 两种屏障语义
    • 三色:白(未标记)、灰(可达、待扫描)、黑(已扫描)。
    • CMS/Parallel 等历史方案常用 Incremental UpdateG1/ZGC 倾向 SATB,便于在并发时维持可达集不漏标。
  • 记忆集(Remembered Set, RSet)
    • 记录“哪些 card/region 有指向本 region 的引用”。G1 的 RSet 用于精准并行回收与 Region 选择。
  • Humongous 对象(G1)
    • 大于 region 一定比例(典型 ≥ 50%)的对象用 H 区域保存,回收/整理策略与普通 region 有差异。
  • 调优方向
    • 吞吐优先:Parallel,尽量大 Eden、少 GC 次数。
    • 低延迟:G1 设置合理 MaxGCPauseMillis;或使用 ZGC(通常无需大量参数)。
    • 观察指标:GC 日志(JDK9+ 用 Unified LoggingXlog:gc*)、停顿时间分布、晋升失败、RSet 扫描时间、混合回收轮次等。

如果这篇文章帮到你了,别忘了点个关注呀!后续会持续更新 Java、Spring、MySQL 等后端干货,一起进阶~

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐