【JVM篇12】:垃圾回收器种类(从分代到并发)

发布于:2025-08-02 ⋅ 阅读:(9) ⋅ 点赞:(0)


垃圾回收器是JVM内存管理的核心组件,它的选择和调优直接影响着Java应用程序的性能表现,尤其是在高并发和低延迟场景下。不同的业务场景需要不同的垃圾收集器才能保证GC性能

就目前主流的HotSpot JVM而言,垃圾收集器主要分为两大类:分代收集器分区收集器

一、分代收集器

分代收集器是基于JVM分代收集理论设计的,它将堆内存划分为新生代和老年代,并针对不同代的特点采用不同的回收算法

1. 新生代垃圾回收器

新生代对象“朝生夕死”的特点使得复制算法成为其理想选择

1.1 Serial收集器

  • 特点
    • 单线程:Serial收集器是最基本、历史最悠久的收集器。它只会使用一条垃圾收集线程进行工作
    • STW(Stop-The-World):在进行垃圾收集时,它必须暂停所有用户线程,直到收集结束
    • 算法:新生代采用复制算法
    • 适用场景:由于其简单高效,且没有线程交互开销,Serial收集器对于运行在Client模式下的虚拟机是一个不错的选择。在单CPU环境下,其专注性和独占性往往有更好的性能表现
  • 参数:通过-XX:+UseSerialGC参数开启

1.2 ParNew收集器

  • 特点
    • 多线程:ParNew收集器是Serial收集器的多线程版本。除了使用多线程,其控制参数、收集算法、回收策略等与Serial收集器完全一样
    • STW:同样会触发STW
    • 算法:新生代采用复制算法
    • 优势:在多CPU环境下,可以充分利用多核优势,更快完成垃圾收集
    • 特殊性:它是目前新生代首选的垃圾回收器之一,因为它是唯一一个能与老年代CMS收集器配合工作的
  • 参数:通过-XX:+UseParNewGC参数开启。默认开启的线程数与CPU数量相同,可通过-XX:ParallelGCThreads设置

1.3 Parallel Scavenge收集器

  • 特点
    • 多线程:与ParNew类似,也是多线程的并行收集器
    • 关注吞吐量:与CMS关注停顿时间不同,Parallel Scavenge收集器更关注系统的吞吐量(CPU用于运行用户代码的时间与CPU总消耗时间的比值)
    • 算法:新生代使用复制算法
    • 自适应调节策略:它提供-XX:+UseAdaptiveSizePolicy参数,开启后JVM会根据当前系统运行情况动态调整新生代大小、Eden与Survivor比例、晋升老年代对象年龄等参数,以达到最佳吞吐量或停顿时间目标。这是它与ParNew的一个重要区别
    • JDK8默认:JDK8默认使用Parallel Scavenge + Parallel Old组合
  • 参数
    • -XX:+UseParallelGC:开启新生代使用此收集器
    • -XX:MaxGCPauseMillis:设置最大垃圾收集停顿时间
    • -XX:GCTimeRatio:设置吞吐量大小,默认99,即GC时间占比不超过1%

2. 老年代垃圾回收器

老年代对象存活率高,不适合复制算法,通常采用标记-清除或标记-整理算法

2.1 Serial Old收集器

  • 特点
    • 单线程:Serial收集器的老年代版本,同样是单线程收集器
    • 算法:使用标记-整理算法
    • 用途
      • 在JDK1.5及之前版本中与Parallel Scavenge收集器搭配使用
      • 作为CMS收集器的后备预案:当CMS出现Concurrent Mode Failure时,会临时启用Serial Old进行Full GC
  • 参数:无特定开启参数,通常随SerialGC或作为CMS后备

2.2 Parallel Old收集器

  • 特点
    • 多线程:Parallel Scavenge收集器的老年代版本,也是多线程收集器
    • 关注吞吐量:与Parallel Scavenge一样,关注系统吞吐量
    • 算法:使用标记-整理算法
    • 用途:与Parallel Scavenge搭配使用,形成“吞吐量优先”的组合,在注重吞吐量和CPU资源敏感的场合优先考虑
  • 参数:通过-XX:+UseParallelOldGC参数开启

2.3 CMS收集器

  • 特点
    • 并发收集、低停顿:CMS是HotSpot虚拟机第一款真正意义上的并发收集器,其目标是获取最短回收停顿时间。它实现了垃圾收集线程与用户线程(基本上)同时工作
    • 算法:基于**“标记-清除”算法**实现
    • 工作流程(四步)
      1. 初始标记:短暂停顿(STW),标记GC Roots直接关联的对象,速度很快
      2. 并发标记:与用户线程并发执行,遍历整个对象图,标记所有可达对象。这是最耗时的阶段,但无需STW
      3. 重新标记:短暂停顿(STW),修正并发标记期间因用户程序运行导致标记变动的对象记录。停顿时间通常比初始标记稍长
      4. 并发清除:与用户线程并发执行,清除已标记为垃圾的对象
    • 缺点(重要)
      1. 对CPU资源敏感:并发阶段会占用CPU资源,可能导致应用程序变慢,总吞吐量降低
      2. 无法处理浮动垃圾:并发清除时,用户线程还在产生新垃圾,这些垃圾无法在当次GC中处理,只能留待下次清理
      3. 内存碎片化:基于“标记-清除”算法,会产生大量不连续的内存碎片,可能导致大对象无法分配而提前触发Full GC
      4. Concurrent Mode Failure:如果预留内存不足,可能导致CMS失败,转而使用Serial Old进行Full GC,造成长时间停顿
    • 状态在JDK9中被标记为弃用(deprecated),并在JDK14中被移除
  • 参数
    • -XX:+UseConcMarkSweepGC:开启CMS
    • -XX:CMSInitiatingOccupancyFraction:设置老年代空间使用率达到多少时触发GC,默认JDK6及以上为92%
    • -XX:+UseCMSCompactAtFullCollection:在Full GC后进行内存碎片整理,但会增加STW时间
    • -XX:CMSFullGCsBeforeCompaction:设置多少次CMS回收后进行一次内存压缩

二、分区收集器

分区收集器不再严格按照分代划分物理内存,而是将堆划分为多个独立的区域,并根据这些区域的特点进行回收

1. G1收集器(默认)

  • 特点
    • JDK9默认:G1在JDK1.7引入,并在JDK9时取代CMS成为默认垃圾收集器
    • 面向服务端:主要针对配备多核处理器及大容量内存的机器,追求在满足GC停顿时间要求的同时具备高吞吐量
    • Region划分:将整个Java堆划分为多个大小相等的独立区域(Region),每个Region都可以是Eden、Survivor或Old区。G1有专门分配大对象的Humongous区
    • 并行与并发:能充分利用多CPU优势缩短STW时间,部分GC动作可并发执行
    • 分代概念保留:虽然内存分区,但仍保留分代的概念,能独立管理整个GC堆
    • 空间整合:从整体看基于标记-整理算法,从局部(Region之间)看基于复制算法。这使得G1不会产生内存碎片
    • 可预测的停顿:G1一大优势是可以建立可预测的停顿时间模型。用户可以指定期望的停顿时间(-XX:MaxGCPauseMillis,默认200ms),G1会根据此目标选择回收价值最大的Region
    • GC模式:G1包含Young GC、Mixed GC和Full GC
      • Young GC:发生在新生代Region的回收
      • Mixed GC:G1特有,回收整个新生代Region以及部分老年代Region。G1通过多次Mixed GC来逐步回收老年代垃圾,以避免Full GC
      • Full GC:当Mixed GC过程中老年代空间不足,或者堆浪费百分比过高时可能触发
  • 工作流程(四步)
    1. 初始标记:短暂停顿(STW),标记从GC Roots直接可达的对象
    2. 并发标记:与应用并发运行,标记所有可达对象
    3. 最终标记先:短暂停顿(STW),处理并发标记阶段结束后残留的引用变更
    4. 筛选回收:根据标记结果,选择回收价值高的Region,复制存活对象到新Region,回收旧区域内存。此阶段包含STW
  • 参数
    • -XX:+UseG1GC:启用G1收集器
    • -XX:G1HeapRegionSize=n:设置Region大小
    • -XX:MaxGCPauseMillis:设置期望的最大停顿时间

2. ZGC收集器

  • 特点
    • 低延迟、大内存:JDK11推出,目标是不超过10ms的停顿时间下,支持TB级内存容量(未来可达16TB)。停顿时间不随堆大小或活跃对象数量增加而增加
    • 并发性高:ZGC在标记、转移和重定位阶段几乎都是并发的,这是其实现低停顿的关键
    • 算法:采用标记-复制算法,但做了重大优化
    • 核心技术
      • 指针染色:在指针中嵌入对象的元数据信息(如是否被移动、存活状态),通过指针颜色即可区分对象状态,无需额外内存访问,加速标记和转移
      • 读屏障:在程序读取对象时插入特殊检查,确保对象访问的正确性。如果对象已被移动,读屏障会返回对象的新地址,从而在并发移动对象时保持内存访问一致性
    • 工作流程:由STW暂停阶段(标记开始、重新映射开始、暂停结束)和并发阶段(并发标记/重新映射、并发引用处理、并发转移准备、并发转移)组成
    • 发展:在Java11中处于试验阶段,Java15正式可用。Java21引入了分代ZGC,进一步缩短停顿时间
  • 参数:通过-XX:+UseZGC启用

三、总结与选择

垃圾收集器 区域 算法 特点 关注点 JDK默认(特定版本)
Serial 新生代/老年代 复制/标记-整理 单线程,STW,简单高效 简单高效 Client模式下默认
ParNew 新生代 复制 多线程版Serial,STW,可与CMS配合 吞吐量 搭配CMS使用
Parallel Scavenge 新生代 复制 多线程,STW,吞吐量优先,支持自适应调节策略 吞吐量 JDK8默认新生代
Serial Old 老年代 标记-整理 单线程,STW,Serial的老年代版本,作为CMS后备 简单高效 作为CMS后备
Parallel Old 老年代 标记-整理 多线程,STW,Parallel Scavenge的老年代版本,与Parallel Scavenge搭配实现高吞吐量 吞吐量 JDK8默认老年代
CMS 老年代 标记-清除 并发收集,低停顿,但有CPU敏感、浮动垃圾、内存碎片缺点,JDK9弃用,JDK14移除 低停顿 JDK8前广泛使用
G1 全堆 标记-整理/复制 分区,可预测停顿,并行与并发,无内存碎片,JDK9默认 可预测停顿 JDK9及更高版本默认
ZGC 全堆 标记-复制 极低延迟(毫秒级),不随堆大小增长,高并发,大内存支持(TB级),采用指针染色和读屏障,JDK11引入,JDK15正式可用 极低延迟

选择合适的垃圾收集器
没有“最好”的垃圾收集器,只有“最适合”的。选择时需要根据具体的应用场景、对吞吐量和延迟的要求、以及可用的硬件资源来权衡

  • 追求极致吞吐量:考虑Parallel Scavenge + Parallel Old组合
  • 追求低停顿时间(但可接受一定碎片和CPU开销):在JDK8及之前,考虑CMS
  • 大内存、多核,且追求可控的低停顿:G1是当前主流且推荐的选择
  • 追求极低延迟,尤其是在超大堆内存场景:ZGC是未来的趋势,适用于对停顿时间要求极其苛刻的场景

网站公告

今日签到

点亮在社区的每一天
去签到