文章目录
垃圾回收器是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虚拟机第一款真正意义上的并发收集器,其目标是获取最短回收停顿时间。它实现了垃圾收集线程与用户线程(基本上)同时工作
- 算法:基于**“标记-清除”算法**实现
- 工作流程(四步):
- 初始标记:短暂停顿(STW),标记GC Roots直接关联的对象,速度很快
- 并发标记:与用户线程并发执行,遍历整个对象图,标记所有可达对象。这是最耗时的阶段,但无需STW
- 重新标记:短暂停顿(STW),修正并发标记期间因用户程序运行导致标记变动的对象记录。停顿时间通常比初始标记稍长
- 并发清除:与用户线程并发执行,清除已标记为垃圾的对象
- 缺点(重要):
- 对CPU资源敏感:并发阶段会占用CPU资源,可能导致应用程序变慢,总吞吐量降低
- 无法处理浮动垃圾:并发清除时,用户线程还在产生新垃圾,这些垃圾无法在当次GC中处理,只能留待下次清理
- 内存碎片化:基于“标记-清除”算法,会产生大量不连续的内存碎片,可能导致大对象无法分配而提前触发Full GC
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过程中老年代空间不足,或者堆浪费百分比过高时可能触发
- 工作流程(四步):
- 初始标记:短暂停顿(STW),标记从GC Roots直接可达的对象
- 并发标记:与应用并发运行,标记所有可达对象
- 最终标记先:短暂停顿(STW),处理并发标记阶段结束后残留的引用变更
- 筛选回收:根据标记结果,选择回收价值高的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是未来的趋势,适用于对停顿时间要求极其苛刻的场景