Parallel 垃圾回收器(也称为 吞吐量优先收集器)。它是 Java 早期(特别是 JDK 8 及之前)在多核处理器上的默认垃圾回收器,其核心设计目标是最大化应用程序的吞吐量。
一、Parallel 回收器的定位与设计目标
-
核心目标:高吞吐量 (High Throughput)
-
吞吐量定义: 应用程序运行时间占总时间(应用程序运行时间 + 垃圾回收时间)的比例。
吞吐量 = 应用运行时间 / (应用运行时间 + GC 时间) * 100%
。 -
设计哲学: 为了最大化应用代码的执行效率,它愿意使用更长时间的 Stop-The-World (STW) 停顿,换取在应用运行时阶段更高的效率和更短的总运行时间。它假设应用可以容忍较长的、但次数较少的 GC 停顿。
-
-
实现方式:并行处理
-
充分利用多核 CPU 的优势,在 STW 期间,使用多个 GC 线程并行执行垃圾回收任务(标记、复制/清除、整理),从而加快单次 GC 的速度。
-
注意: 这里的“并行”(Parallel) 指的是 GC 线程之间的并行(多个 GC 线程同时工作),不是 GC 线程与应用程序线程的并发(Concurrent)。Parallel 回收器在进行垃圾回收时,必须暂停所有应用线程 (STW)。
-
-
分代收集: 遵循分代假说,将堆划分为年轻代 (Young Generation) 和老年代 (Old Generation),并分别使用不同的并行回收器:
-
年轻代:
Parallel Scavenge
(并行复制) -
老年代:
Parallel Old
(并行标记-整理) (在 JDK 6u18 之前,老年代搭配的是单线程的Serial Old
,之后Parallel Old
成为标准搭配)
-
-
适用场景:
-
适合后台计算密集型任务,如科学计算、批处理作业、报表生成等。
-
应用对吞吐量要求极高,对单个 GC 停顿时间不太敏感(可以接受几百毫秒甚至秒级的停顿)。
-
运行在多核/多 CPU 的服务器上。
-
二、核心组件与算法
-
年轻代:Parallel Scavenge (并行复制)
-
算法: 复制算法 (Copying)
-
堆结构: 年轻代划分为一个较大的
Eden
区和两个较小的Survivor
区 (From
,To
)。默认比例Eden : Survivor = 8:1
(可通过-XX:SurvivorRatio
调整)。 -
回收过程 (STW):
-
触发条件:Eden 区满。
-
STW: 暂停所有应用线程。
-
并行标记: 多个 GC 线程并行地从 GC Roots 出发,标记 Eden 区和当前
From
Survivor 区中的存活对象。 -
并行复制: 多个 GC 线程并行地将标记出的存活对象复制到
To
Survivor 区。-
新对象直接在 Eden 区分配。
-
对象在 Survivor 区之间每熬过一次 GC,年龄增加 1。
-
达到晋升年龄阈值 (
-XX:MaxTenuringThreshold
) 的对象会被晋升 (Promote) 到老年代。 -
如果
To
Survivor 区空间不足以容纳所有存活对象,或者存活对象年龄过大,会直接晋升到老年代。
-
-
清空与交换: 清空 Eden 区和刚使用完的
From
Survivor 区。交换From
和To
的角色(下次 GC 时,当前的To
变成新的From
)。
-
-
特点: 高效、简单,没有内存碎片。STW 时间与存活对象数量成正比。
-
-
老年代:Parallel Old (并行标记-整理)
-
算法: 标记-整理 (Mark-Compact)
-
回收过程 (STW):
-
触发条件:
-
显式调用
System.gc()
(通常不建议)。 -
老年代空间不足(例如,年轻代晋升失败,或者大对象直接进入老年代导致空间不足)。
-
元空间 (Metaspace) / 永久代 (PermGen) 空间不足(会连带触发 Full GC)。
-
-
STW: 暂停所有应用线程。
-
并行标记: 多个 GC 线程并行地从 GC Roots 出发,递归遍历对象图,标记老年代中所有存活对象。
-
并行计算整理位置: 多个 GC 线程并行地计算每个存活对象在整理后应该移动到的目标地址(通常是基于区域划分,每个线程负责一块连续区域)。
-
并行整理: 多个 GC 线程并行地根据上一步计算出的目标地址,将存活对象复制(移动)到新的位置。这相当于将存活对象“滑动”到堆的一端。
-
并行清除: 多个 GC 线程并行地更新所有指向被移动对象的引用(指针)。最后,清除掉整理后边界以外的所有空间(即死亡对象占用的空间)。
-
-
特点:
-
解决了内存碎片问题: 整理过程将存活对象紧密排列在堆的一端,腾出大块的连续空间,消除了内存碎片。
-
STW 时间较长: 整个标记-整理过程需要在 STW 下完成,且涉及全堆扫描和对象移动,对于大堆来说停顿时间可能相当可观(秒级)。
-
吞吐量高: 并行化极大地加速了整个回收过程,相比单线程的 Serial Old 快很多。
-
-
三、核心特性与优势
-
高吞吐量: 这是其最主要也是最大的优势。通过并行化 GC 任务,最大限度地减少了 GC 本身所占用的时间(尽管每次停顿时间长,但频率相对较低),使得应用线程有更多的时间执行业务逻辑,特别适合需要处理大量数据、完成繁重计算任务的场景。
-
高效利用多核 CPU: 在 STW 期间,它能让所有可用的 CPU 核心全力投入垃圾回收工作,充分利用硬件资源加速 GC。
-
内存碎片控制 (Parallel Old): 老年代使用标记-整理算法,避免了像 CMS 那样的内存碎片问题,不会因为碎片导致分配失败而触发更耗时的 Full GC。
-
成熟稳定: 作为 JDK 8 及之前的默认回收器,经过长期发展和优化,非常成熟稳定。
四、缺点与挑战
-
较长的 STW 停顿时间: 这是追求高吞吐量付出的代价。无论是年轻代的 Parallel Scavenge 还是老年代的 Parallel Old,在进行回收时都必须暂停所有应用线程,且停顿时间会随着堆大小的增加而增加。这对于需要低延迟、快速响应的应用(如 Web 服务、实时交易系统)是不可接受的。
-
暂停时间不可预测: 停顿时间的长短主要取决于堆中存活对象的数量和堆的大小,不像 G1 或 CMS 那样有明确的停顿时间目标模型。停顿时间波动可能较大。
-
缺乏并发性: GC 线程与应用线程不能同时工作。在 GC 发生时,应用完全停止。
-
调优相对复杂 (主要针对吞吐量目标):
-
需要平衡堆大小、各代比例与 GC 频率/停顿时间的关系。
-
核心调优参数围绕吞吐量和停顿时间目标设定。
-
五、关键配置参数
-
启用 Parallel 回收器:
-
JDK 8 及之前:默认启用。或显式指定
-XX:+UseParallelGC
(启用 Parallel Scavenge + Serial Old) 或-XX:+UseParallelOldGC
(启用 Parallel Scavenge + Parallel Old, 推荐)。在 JDK 8 中,UseParallelGC
默认会激活UseParallelOldGC
。 -
JDK 9+:不再是默认回收器 (G1 是默认),需显式指定
-XX:+UseParallelGC
或-XX:+UseParallelOldGC
。
-
-
设置 GC 线程数:
-
-XX:ParallelGCThreads=<n>
: 设置用于年轻代和老年代并行 GC 的线程数。默认值通常等于 CPU 核心数。对于 CPU 密集型应用,设置接近核心数可最大化并行效率;对于 IO 密集型或 CPU 核心非常多的情况,可以适当减少以避免过度切换。
-
-
吞吐量目标 (首要目标):
-
-XX:GCTimeRatio=<ratio>
: 最重要的吞吐量控制参数。 设置期望的 GC 时间与应用程序时间之比。公式为应用运行时间 = GCTimeRatio * GC 时间
。默认值99
表示应用时间 : GC 时间 = 99 : 1
,即吞吐量目标为99%
(1 - 1/(1+99) = 0.99
)。增大此值(如 99 -> 199)表示允许更少的 GC 时间(更高的吞吐量),JVM 会尝试通过增大堆空间(减少 GC 频率)或更激进地回收(可能增加单次停顿时间)来实现。
-
-
最大 GC 停顿时间目标 (次要目标/软目标):
-
-XX:MaxGCPauseMillis=<ms>
: 设置期望的每次 GC 停顿的最大毫秒数。这是一个软目标 (Soft Goal),JVM 会尽力达成,但不保证绝对满足,且优先级低于GCTimeRatio
。默认值不设置。设置一个合理的值(如 100-500ms)可以指导 JVM 调整堆和代的大小(例如,为了减少单次停顿时间,可能会缩小年轻代,导致更频繁但更短的 Young GC)。
-
-
堆大小与代大小:
-
-Xms
/-Xmx
: 设置堆的初始大小和最大大小。通常设置为相同值以避免堆大小动态调整的开销,这对吞吐量应用很关键。 -
-XX:NewRatio=<ratio>
: 设置老年代与年轻代的比例。例如-XX:NewRatio=3
表示老年代:年轻代 = 3:1
(年轻代占堆的 1/4)。 -
-XX:NewSize=<size>
/-XX:MaxNewSize=<size>
: 直接设置年轻代的初始大小和最大大小 (优先级高于NewRatio
)。 -
-XX:SurvivorRatio=<ratio>
: 设置 Eden 区与一个 Survivor 区的比例。例如-XX:SurvivorRatio=8
表示Eden : Survivor = 8:1
(每个 Survivor 占年轻代的 1/10)。
-
-
晋升年龄控制:
-
-XX:MaxTenuringThreshold=<age>
: 设置对象晋升到老年代前在年轻代经历的最大 GC 次数(年龄)。默认值15
。增大此值可以让对象在年轻代停留更久,减少晋升到老年代的数量,从而减少 Full GC 频率。但设置过大可能导致 Survivor 区溢出或对象在多次 Young GC 中反复复制。
-
六、调优注意事项
-
优先保障吞吐量 (
GCTimeRatio
): 这是 Parallel 回收器的核心目标。调优应首先关注GCTimeRatio
是否达到预期。 -
合理设置堆大小 (
-Xms
=-Xmx
): 足够大的堆可以减少 GC 频率。但过大的堆会导致单次 GC 停顿时间更长。找到平衡点。 -
监控 GC 日志: 使用
-Xlog:gc*
(JDK 9+) 或-verbose:gc
+-XX:+PrintGCDetails
+-XX:+PrintGCDateStamps
(JDK 8) 等参数输出详细 GC 日志。分析:-
实际吞吐量 (
应用时间 / 总时间
)。 -
Young GC / Full GC 的频率和平均/最大停顿时间。
-
各代空间占用情况、晋升情况。
-
-
理解
MaxGCPauseMillis
的副作用: 为了达到更短的停顿目标,JVM 可能会:-
缩小年轻代 → 增加 Young GC 频率(虽然每次短了,但总次数多了)。
-
降低晋升年龄阈值 → 增加晋升到老年代的对象 → 增加 Full GC 频率。
-
这些调整可能损害吞吐量 (
GCTimeRatio
)! 设置MaxGCPauseMillis
需谨慎,并监控其对吞吐量的影响。
-
-
避免过早晋升: 确保 Survivor 区足够大 (
SurvivorRatio
),并且MaxTenuringThreshold
设置合理,避免大量“朝生夕死”的对象过早进入老年代触发 Full GC。
七、与 CMS/G1/ZGC/Shenandoah 的对比
-
vs CMS: CMS 追求低延迟(减少 STW 时间),使用并发标记清除(有碎片问题)。Parallel 追求高吞吐量,使用并行 STW 回收(无碎片)。CMS 在 JDK 14+ 已移除。
-
vs G1: G1 也利用并行性,但核心目标是可预测的低停顿,同时兼顾高吞吐量。它采用 Region 化堆和部分回收,支持并发标记,停顿时间模型更可控。G1 是 JDK 9+ 的默认回收器,更适合需要平衡吞吐量和延迟的大堆应用。
-
vs ZGC / Shenandoah: 新一代超低延迟回收器,停顿时间目标是 亚毫秒级 (<10ms),且几乎不随堆大小增长。它们使用了染色指针、读屏障等更先进的技术实现高度并发。适用于超大堆和极致延迟要求的场景。它们也追求高吞吐量,但在极高吞吐量场景下,Parallel 可能仍有微弱的理论优势(因为并发回收器有运行时屏障开销)。
八、总结
Parallel 垃圾回收器(Parallel Scavenge + Parallel Old)是 JVM 中经典的吞吐量优先解决方案。其核心优势在于:
-
最大化应用程序吞吐量: 通过并行化 STW 期间的垃圾回收任务,充分利用多核 CPU 资源,最小化 GC 本身占用的总时间。
-
高效稳定: 成熟可靠,特别适合计算密集型、批处理型应用。
-
内存整理 (Parallel Old): 避免老年代碎片问题。
其主要代价是:
-
较长的、不可预测的 STW 停顿: 不适合延迟敏感型应用。
-
缺乏并发处理能力。
适用场景: 当应用的核心需求是用最短的总时间完成尽可能多的工作任务(高吞吐量),并且可以容忍秒级或几百毫秒级的、偶发的暂停(如后台报表生成、科学计算、离线数据分析)时,Parallel 回收器是一个非常好的选择,尤其是在 JDK 8 环境中。在新版本 JDK (9+) 中,虽然 G1 是默认且更通用,但如果吞吐量是绝对优先指标且停顿可接受,Parallel 仍然是值得考虑的选项。对于需要极低延迟或超大堆的应用,则应考虑 G1、ZGC 或 Shenandoah。