# Java 中常见的垃圾收集器有哪些?

7 min read
Table of Contents

Java 中常见的垃圾收集器有哪些?

Java 常见垃圾收集器面试标准答案

【核心定义】

Java 垃圾收集器是JVM内存管理系统的核心组件,负责自动回收不再使用的对象以释放内存,不同的收集器在吞吐量、延迟和内存占用之间有不同的权衡策略。

【关键要点】

  1. Serial 收集器:单线程串行收集器,新生代采用复制算法,老年代采用标记-整理算法,适用于客户端应用或资源受限环境。
  2. Parallel Scavenge/Parallel Old:多线程并行收集器,新生代采用复制算法,老年代采用标记-整理算法,以吞吐量优先为目标。
  3. ParNew 收集器:Serial 收集器的多线程版本,新生代收集器,常与CMS配合使用。
  4. CMS(Concurrent Mark-Sweep):并发标记清除收集器,老年代收集器,以最短停顿时间为目标,采用标记-清除算法。
  5. G1(Garbage-First):面向服务端应用的收集器,采用分区算法,可预测停顿时间,JDK 9+成为默认收集器。
  6. ZGC(Z Garbage Collector):JDK 11引入的低延迟收集器,停顿时间不超过10ms,支持TB级堆内存。
  7. Shenandoah:Red Hat开发的低延迟收集器,与ZGC竞争,JDK 12引入。

【深度推导/细节】

核心矛盾:吞吐量 vs 延迟

  • 吞吐量优先:Parallel Scavenge系列,适合后台计算型应用(如批处理),GC时间占总运行时间的比例最小化
  • 低延迟优先:CMS/G1/ZGC/Shenandoah,适合交互式应用(如Web服务),单次GC停顿时间最小化

关键数字与设计合理性

  1. CMS的4阶段设计:

    • 初始标记(STW):标记GC Roots直接关联对象,停顿短
    • 并发标记:与用户线程并发执行,标记所有可达对象
    • 重新标记(STW):修正并发标记期间的变化,停顿较短
    • 并发清除:与用户线程并发执行,清理垃圾对象
  2. G1的分区与预测模型:

    • 将堆划分为多个Region(默认2048个)
    • 维护每个Region的回收价值(回收空间大小/回收时间)
    • 每次回收价值最高的Region(Garbage-First名称来源)
    • 可设置期望停顿时间(-XX:MaxGCPauseMillis )
  3. ZGC的染色指针技术:

    • 在指针中嵌入元数据(4位),标记对象状态
    • 实现并发转移,无需STW的压缩阶段
    • 支持NUMA-aware内存分配

【关联/对比】

收集器组合关系

新生代收集器 老年代收集器 适用场景
Serial Serial Old 客户端/小内存
Parallel Scavenge Parallel Old 吞吐量优先
ParNew CMS 低延迟(JDK 8及之前)
G1(统一管理) 平衡型(JDK 9+默认)

算法演进对比

收集器算法特点停顿时间吞吐量内存碎片
Serial标记-整理/复制长中等无
Parallel并行标记-整理中等高无
CMS并发标记-清除短中等有
G1分区标记-整理可预测高较少
ZGC并发标记-复制转移极短高无

版本演进路线

  • JDK 7/8:Parallel为默认,CMS为低延迟选择
  • JDK 9:G1成为默认收集器
  • JDK 11:引入ZGC(实验性)
  • JDK 15:ZGC和Shenandoah转为正式功能
  • JDK 21:ZGC性能大幅优化,支持分代收集

【线程安全与并发机制】

  1. 安全点(Safepoint)机制:所有收集器都需要在安全点暂停用户线程
  2. 并发 vs 并行:
    • 并行:多个GC线程同时工作(Parallel)
    • 并发:GC线程与用户线程同时工作(CMS/G1/ZGC)
  3. 写屏障(Write Barrier):
    • CMS:增量更新(Incremental Update)
    • G1:SATB(Snapshot-At-The-Beginning)
    • ZGC:负载屏障(Load Barrier)

『面试官追问』

高频问题1:CMS为什么被废弃?

  • 内存碎片问题:标记-清除算法产生碎片,Full GC时压缩停顿长
  • 并发模式失败:并发期间老年代空间不足,退化为Serial Old
  • 浮动垃圾:并发清理期间新产生的垃圾无法回收
  • JDK 9被标记为废弃,JDK 14正式移除

高频问题2:G1如何实现可预测停顿?

Step 1: 初始标记(STW) - 标记GC Roots直接引用
Step 2: 根区域扫描 - 扫描Survivor区到老年代引用
Step 3: 并发标记 - 标记整个堆的可达对象
Step 4: 最终标记(STW) - 处理SATB记录
Step 5: 筛选回收 - 根据停顿预测模型选择Region回收

关键:维护Region的回收价值优先队列,每次选择性价比最高的Region回收。

高频问题3:ZGC vs G1性能对比

  • 停顿时间:ZGC < 10ms,G1通常10-200ms
  • 吞吐量损失:ZGC约15%,G1约10%
  • 内存占用:ZGC需要额外内存存储转发表
  • 适用场景:ZGC适合超大堆和严格延迟要求,G1适合通用场景

高频问题4:如何选择收集器?

  1. 小堆/客户端:Serial(-XX:+UseSerialGC)
  2. 吞吐量优先:Parallel(-XX:+UseParallelGC)
  3. 低延迟(JDK 8):ParNew+CMS(-XX:+UseParNewGC -XX:+UseConcMarkSweepGC)
  4. 平衡型(JDK 8+):G1(-XX:+UseG1GC)
  5. 超大堆/极低延迟:ZGC(-XX:+UseZGC)或Shenandoah(-XX:+UseShenandoahGC)

高频问题5:分代ZGC(JDK 21)的优势

  • 年轻代对象朝生夕死,单独收集效率更高
  • 减少老年代扫描压力
  • 降低写屏障开销
  • 整体吞吐量提升,停顿时间进一步降低

最佳实践:生产环境应根据应用特点(堆大小、延迟要求、吞吐量需求)进行压测,选择最适合的收集器。JDK 11+推荐从G1开始,如有严格延迟要求再考虑ZGC。

My avatar

文章看完了,不打算去别的篇章转转吗?👀 感谢你的耐心阅读!如果你有问题想探讨,或者发现了文中的 Bug,请务必通过页脚的链接联系我


More Posts

# Java 中如何创建多线程?

8 min read

Java 中创建多线程的本质是让一个任务(Runnable 或 Callable)在独立的执行路径上并发运行,主要可通过继承 Thread 类、实现 Runnable 接口、实现 Callable 接口结合 ExecutorService 线程池,以及利用 CompletableFuture 这四种核心方式实现。

Read

Comments