当前位置: 首页 > news >正文

JVM(10)——详解Parallel垃圾回收器

Parallel 垃圾回收器(也称为 吞吐量优先收集器)。它是 Java 早期(特别是 JDK 8 及之前)在多核处理器上的默认垃圾回收器,其核心设计目标是最大化应用程序的吞吐量

一、Parallel 回收器的定位与设计目标

  1. 核心目标:高吞吐量 (High Throughput)

    • 吞吐量定义: 应用程序运行时间占总时间(应用程序运行时间 + 垃圾回收时间)的比例。吞吐量 = 应用运行时间 / (应用运行时间 + GC 时间) * 100%

    • 设计哲学: 为了最大化应用代码的执行效率,它愿意使用更长时间的 Stop-The-World (STW) 停顿,换取在应用运行时阶段更高的效率和更短的总运行时间。它假设应用可以容忍较长的、但次数较少的 GC 停顿。

  2. 实现方式:并行处理

    • 充分利用多核 CPU 的优势,在 STW 期间,使用多个 GC 线程并行执行垃圾回收任务(标记、复制/清除、整理),从而加快单次 GC 的速度。

    • 注意: 这里的“并行”(Parallel) 指的是 GC 线程之间的并行(多个 GC 线程同时工作),不是 GC 线程与应用程序线程的并发(Concurrent)。Parallel 回收器在进行垃圾回收时,必须暂停所有应用线程 (STW)

  3. 分代收集: 遵循分代假说,将堆划分为年轻代 (Young Generation) 和老年代 (Old Generation),并分别使用不同的并行回收器:

    • 年轻代: Parallel Scavenge (并行复制)

    • 老年代: Parallel Old (并行标记-整理) (在 JDK 6u18 之前,老年代搭配的是单线程的 Serial Old,之后 Parallel Old 成为标准搭配)

  4. 适用场景:

    • 适合后台计算密集型任务,如科学计算、批处理作业、报表生成等。

    • 应用对吞吐量要求极高,对单个 GC 停顿时间不太敏感(可以接受几百毫秒甚至秒级的停顿)。

    • 运行在多核/多 CPU 的服务器上。

二、核心组件与算法

  1. 年轻代:Parallel Scavenge (并行复制)

    • 算法: 复制算法 (Copying)

    • 堆结构: 年轻代划分为一个较大的 Eden 区和两个较小的 Survivor 区 (FromTo)。默认比例 Eden : Survivor = 8:1 (可通过 -XX:SurvivorRatio 调整)。

    • 回收过程 (STW):

      1. 触发条件:Eden 区满。

      2. STW: 暂停所有应用线程。

      3. 并行标记: 多个 GC 线程并行地从 GC Roots 出发,标记 Eden 区和当前 From Survivor 区中的存活对象。

      4. 并行复制: 多个 GC 线程并行地将标记出的存活对象复制到 To Survivor 区。

        • 新对象直接在 Eden 区分配。

        • 对象在 Survivor 区之间每熬过一次 GC,年龄增加 1。

        • 达到晋升年龄阈值 (-XX:MaxTenuringThreshold) 的对象会被晋升 (Promote) 到老年代。

        • 如果 To Survivor 区空间不足以容纳所有存活对象,或者存活对象年龄过大,会直接晋升到老年代。

      5. 清空与交换: 清空 Eden 区和刚使用完的 From Survivor 区。交换 From 和 To 的角色(下次 GC 时,当前的 To 变成新的 From)。

    • 特点: 高效、简单,没有内存碎片。STW 时间与存活对象数量成正比。

  2. 老年代:Parallel Old (并行标记-整理)

    • 算法: 标记-整理 (Mark-Compact)

    • 回收过程 (STW):

      1. 触发条件:

        • 显式调用 System.gc() (通常不建议)。

        • 老年代空间不足(例如,年轻代晋升失败,或者大对象直接进入老年代导致空间不足)。

        • 元空间 (Metaspace) / 永久代 (PermGen) 空间不足(会连带触发 Full GC)。

      2. STW: 暂停所有应用线程。

      3. 并行标记: 多个 GC 线程并行地从 GC Roots 出发,递归遍历对象图,标记老年代中所有存活对象。

      4. 并行计算整理位置: 多个 GC 线程并行地计算每个存活对象在整理后应该移动到的目标地址(通常是基于区域划分,每个线程负责一块连续区域)。

      5. 并行整理: 多个 GC 线程并行地根据上一步计算出的目标地址,将存活对象复制(移动)到新的位置。这相当于将存活对象“滑动”到堆的一端。

      6. 并行清除: 多个 GC 线程并行地更新所有指向被移动对象的引用(指针)。最后,清除掉整理后边界以外的所有空间(即死亡对象占用的空间)。

    • 特点:

      • 解决了内存碎片问题: 整理过程将存活对象紧密排列在堆的一端,腾出大块的连续空间,消除了内存碎片。

      • STW 时间较长: 整个标记-整理过程需要在 STW 下完成,且涉及全堆扫描和对象移动,对于大堆来说停顿时间可能相当可观(秒级)。

      • 吞吐量高: 并行化极大地加速了整个回收过程,相比单线程的 Serial Old 快很多。

三、核心特性与优势

  1. 高吞吐量: 这是其最主要也是最大的优势。通过并行化 GC 任务,最大限度地减少了 GC 本身所占用的时间(尽管每次停顿时间长,但频率相对较低),使得应用线程有更多的时间执行业务逻辑,特别适合需要处理大量数据、完成繁重计算任务的场景。

  2. 高效利用多核 CPU: 在 STW 期间,它能让所有可用的 CPU 核心全力投入垃圾回收工作,充分利用硬件资源加速 GC。

  3. 内存碎片控制 (Parallel Old): 老年代使用标记-整理算法,避免了像 CMS 那样的内存碎片问题,不会因为碎片导致分配失败而触发更耗时的 Full GC。

  4. 成熟稳定: 作为 JDK 8 及之前的默认回收器,经过长期发展和优化,非常成熟稳定。

四、缺点与挑战

  1. 较长的 STW 停顿时间: 这是追求高吞吐量付出的代价。无论是年轻代的 Parallel Scavenge 还是老年代的 Parallel Old,在进行回收时都必须暂停所有应用线程,且停顿时间会随着堆大小的增加而增加。这对于需要低延迟、快速响应的应用(如 Web 服务、实时交易系统)是不可接受的。

  2. 暂停时间不可预测: 停顿时间的长短主要取决于堆中存活对象的数量和堆的大小,不像 G1 或 CMS 那样有明确的停顿时间目标模型。停顿时间波动可能较大。

  3. 缺乏并发性: GC 线程与应用线程不能同时工作。在 GC 发生时,应用完全停止。

  4. 调优相对复杂 (主要针对吞吐量目标):

    • 需要平衡堆大小、各代比例与 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 中反复复制。

六、调优注意事项

  1. 优先保障吞吐量 (GCTimeRatio): 这是 Parallel 回收器的核心目标。调优应首先关注 GCTimeRatio 是否达到预期。

  2. 合理设置堆大小 (-Xms=-Xmx): 足够大的堆可以减少 GC 频率。但过大的堆会导致单次 GC 停顿时间更长。找到平衡点。

  3. 监控 GC 日志: 使用 -Xlog:gc* (JDK 9+) 或 -verbose:gc + -XX:+PrintGCDetails + -XX:+PrintGCDateStamps (JDK 8) 等参数输出详细 GC 日志。分析:

    • 实际吞吐量 (应用时间 / 总时间)。

    • Young GC / Full GC 的频率和平均/最大停顿时间。

    • 各代空间占用情况、晋升情况。

  4. 理解 MaxGCPauseMillis 的副作用: 为了达到更短的停顿目标,JVM 可能会:

    • 缩小年轻代 → 增加 Young GC 频率(虽然每次短了,但总次数多了)。

    • 降低晋升年龄阈值 → 增加晋升到老年代的对象 → 增加 Full GC 频率。

    • 这些调整可能损害吞吐量 (GCTimeRatio)! 设置 MaxGCPauseMillis 需谨慎,并监控其对吞吐量的影响。

  5. 避免过早晋升: 确保 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。

http://www.xdnf.cn/news/1064629.html

相关文章:

  • python高校教务管理系统
  • 超详细YOLOv8/11图像菜品分类全程概述:环境、数据准备、训练、验证/预测、onnx部署(c++/python)详解
  • TypeScript类型定义:Interface与Type的全面对比与使用场景
  • 【HarmonyOS Next之旅】DevEco Studio使用指南(三十六) -> 配置构建(三)
  • 算法导论第二十五章 深度学习的伦理与社会影响
  • C4.5算法深度解析:决策树进化的里程碑
  • 怎么让二级域名绑定到wordpesss指定的页面
  • 0-机器学习简介
  • winform mvvm
  • opencv 之双目立体标定算法核心实现
  • STM32F103C8T6,窗口看门狗(WWDG)与独立看门狗(IWDG)详解
  • all()函数和any()函数
  • 【机器学习四大核心任务类型详解】分类、回归、聚类、降维智能决策指南
  • 【投稿与写作】overleaf 文章转投arxiv流程经验分享
  • 开发语言本身只是提供了一种解决问题的工具
  • Windows 后渗透中可能会遇到的加密字符串分析
  • C++结构体初始化与成员函数实现语法详解
  • webpack+vite前端构建工具 -6从loader本质看各种语言处理 7webpack处理html
  • c#websocket心跳包自定义实现,支持异步操作的取消
  • RN(React Native)技术应用中常出现的错误及解决办法
  • 可理解性输入:洗澡习惯
  • 【设计模式】策略模式 在java中的应用
  • 《Redis》事务
  • idea2023+zulu-jdk+maven3.9.10
  • 【后端】负载均衡
  • 解决OSS存储桶未创建导致的XML错误
  • LLMs之MCP:excel-mcp-server的简介、安装和使用方法、案例应用之详细攻略
  • 5.3 VSCode使用FFmpeg库
  • 一,python语法教程.内置API
  • modelscope设置默认模型路径