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

深入解析 HotSpot 的经典垃圾收集器

在 HotSpot 虚拟机中,垃圾收集器是内存回收的实践者,而垃圾收集算法则提供了方法论。从理论上看,我们可以将垃圾收集算法分为引用计数式和追踪式两大类,但在主流 Java 虚拟机中,追踪式垃圾收集器才是实际采用的方案。本文将重点介绍 JDK 7 Update 4 之后、JDK 11 发布之前 Oracle JDK 中的经典垃圾收集器。所谓“经典”是指这些收集器虽然在技术上不再处于前沿,但经过实践千锤百炼,足够成熟、稳定,能够在未来两三年内放心用于商用生产环境。

图 3-6 展示了 HotSpot 虚拟机中各收集器的搭配关系,不同收集器分别适用于新生代与老年代。接下来将逐一介绍这些收集器的目标、特性、原理和使用场景,并重点分析 CMS 和 G1 这两款相对复杂而又广泛使用的收集器。


一、Serial 收集器

1.1 概述

Serial 收集器是 HotSpot 虚拟机中历史最悠久、最基础的垃圾收集器。在 JDK 1.3.1 之前,它曾是新生代收集器的唯一选择。顾名思义,Serial 收集器以单线程方式工作,在进行垃圾收集时会暂停所有其他用户线程,即所谓的“Stop The World”。

1.2 特点与优势

  • 单线程执行
    由于只有一个垃圾收集线程,不涉及线程间的交互和同步,因此其内存回收过程简单高效。

  • 最小的内存占用
    Serial 收集器额外的内存消耗(Memory Footprint)最小,适合内存资源受限的环境。

  • 适用场景
    主要用于客户端模式、单核或低核数处理器环境下的桌面应用和部分微服务应用。对于分配几十 MB 到一两百 MB 新生代的场景,停顿时间可控制在几十到一百多毫秒内,对用户体验影响较小。

1.3 运行示意图

图 3-7 展示了 Serial/Serial Old 收集器的运行过程。当垃圾收集启动时,所有用户线程立即暂停,垃圾收集线程独自运行,完成垃圾标记和回收后,再恢复用户线程。

尽管 Serial 收集器因停顿(Stop The World)的存在给用户带来一定的不便,但在许多桌面和轻量级应用中,其简单高效的特点依然具有较大优势。


二、ParNew 收集器

2.1 概述

ParNew 收集器可以看作是 Serial 收集器的多线程并行版本。它与 Serial 收集器在大部分行为上完全一致,包括对象分配规则、控制参数(如 -XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure 等)、收集算法和回收策略。但 ParNew 利用了多线程并行进行垃圾收集,旨在提高新生代垃圾收集的吞吐量。

2.2 特点与搭配

  • 并行收集
    ParNew 收集器默认开启的垃圾收集线程数与处理器核心数相同,在多核环境中可以充分利用并行处理能力。

  • 与 CMS 搭配
    在服务端模式下,新生代一般采用 ParNew 收集器,而老年代则使用 CMS 收集器。直到 CMS 出现前,ParNew 一直是新生代的默认选择。JDK 5 中 CMS 的出现巩固了 ParNew 的地位,因为除了 Serial 收集器外,当前只有 ParNew 能与 CMS 配合工作。

  • 局限性
    在单核或低核数的环境中,由于线程间交互的开销,ParNew 的效果可能不如 Serial 收集器,因此在这种场景下可能会选择 Serial 收集器。


三、Parallel Scavenge 收集器

3.1 概述

Parallel Scavenge 收集器同样是一款基于标记-复制算法的新生代收集器,但它的设计目标与 ParNew 略有不同。Parallel Scavenge 更关注整体系统的吞吐量,即用户代码运行时间与总执行时间的比例。

3.2 主要特点

  • 吞吐量优先
    Parallel Scavenge 收集器的目标是最大化处理器用于运行用户代码的时间。其停顿时间和垃圾收集时间是可控的,但优化重点在于提高吞吐量。

  • 参数调优

    • -XX:MaxGCPauseMillis
      用户可以指定垃圾收集的最大停顿时间,收集器会尽量使回收时间不超过该值。但需注意,设置更低的停顿目标可能导致新生代空间较小,从而使垃圾收集更频繁,反而降低吞吐量。

    • -XX:GCTimeRatio
      该参数用于设置垃圾收集时间占总时间的比例。例如,设置为 19 意味着垃圾收集时间最多占总时间的 5%(1/(1+19)),默认值为 99(约 1%)。

  • 自适应调节策略
    Parallel Scavenge 收集器支持 -XX:+UseAdaptiveSizePolicy 参数,在开启后,虚拟机会自动根据系统运行情况调整新生代大小、Eden 与 Survivor 区的比例,以及晋升到老年代的对象大小。这样,GC 参数的调优任务可以交给虚拟机自动完成,帮助达到最佳平衡。

3.3 应用场景

吞吐量优先的场景:适合后台大数据处理和批处理任务,用户交互要求不高,但希望高效利用 CPU 资源完成任务。


四、Serial Old 收集器

4.1 概述

Serial Old 收集器是 Serial 收集器的老年代版本,它同样以单线程方式工作,但采用了标记-整理算法。这款收集器主要用于客户端模式下的 HotSpot 虚拟机,也可作为服务端模式下 CMS 收集器失败时的后备方案(例如在 Concurrent Mode Failure 时启用)。

4.2 特点

  • 单线程老年代回收
    由于只使用一个线程进行回收,避免了线程间的同步开销,但也无法充分利用多核处理器。

  • 适用场景
    适合内存较小的桌面应用和客户端应用;在服务端模式下,它可能作为 CMS 失败时的备用方案出现。

  • 代码共享
    在很多官方资料中,PS MarkSweep 收集器(Parallel Scavenge 的老年代收集器)与 Serial Old 实现几乎相同,因此常用 Serial Old 来讲解老年代收集的基本原理。


五、Parallel Old 收集器

5.1 概述

Parallel Old 收集器是 Parallel Scavenge 收集器在老年代的并行版本,采用标记-整理算法实现。它自 JDK 6 开始提供,旨在解决老年代单线程收集的瓶颈问题。

5.2 特点与优势

  • 多线程并行回收
    充分利用多核处理器,降低了老年代回收的停顿时间。

  • 吞吐量优化
    在老年代内存空间较大、硬件性能较高的环境中,Parallel Old 收集器能够在保证整体吞吐量的同时,高效回收垃圾。

  • 搭配优势
    与 Parallel Scavenge 收集器搭配,可以构成一个完整的吞吐量优先的垃圾收集解决方案。

5.3 局限性

  • 实现复杂性
    在处理大规模并行回收时,可能面临更多线程调度与同步问题。

  • 与 CMS 的比较
    在某些情况下,Parallel Old 的整体吞吐量未必比 ParNew + CMS 组合更高,具体效果取决于应用场景和硬件配置。


六、CMS 收集器(Concurrent Mark Sweep)

6.1 概述

CMS 收集器是一款以缩短垃圾收集停顿时间为目标的老年代收集器。它首次实现了垃圾收集线程与用户线程几乎并发工作的模式,极大提升了服务端应用的响应速度。

6.2 运行过程

CMS 收集器的回收过程分为四个阶段:

  1. 初始标记(Initial Mark)
    标记 GC Roots 直接关联的对象。此阶段非常快,需要“Stop The World”,通常与 Minor GC 同步完成。

  2. 并发标记(Concurrent Mark)
    从初始标记的对象开始,遍历整个对象图进行并发标记,不暂停用户线程,但持续记录引用变化(使用写屏障)。

  3. 重新标记(Remark)
    对并发标记结束后仍未标记完全的对象进行修正,此阶段需要短暂暂停用户线程,以确保一致性。

  4. 并发清除(Concurrent Sweep)
    回收未被标记的对象,此阶段与并发标记类似,可与用户线程并发执行。

6.3 CMS 的缺点

  • 资源敏感性
    并发阶段需要消耗部分 CPU 资源,若处理器核心不足,可能显著影响用户线程性能。

  • 浮动垃圾问题
    在并发标记与清除期间,用户线程仍在运行,可能产生新垃圾,但这些垃圾可能未能在当前周期内回收,造成“浮动垃圾”,最终可能触发 Full GC。

  • 内存碎片化
    基于标记-清除算法,回收后可能产生内存碎片。为解决这一问题,CMS 提供了额外的内存整理选项(如 -XX:+UseCMSCompactAtFullCollection 和 -XX:CMSFullGCsBeforeCompaction 参数),但这会增加停顿时间。

6.4 增量式 CMS(i-CMS)

为了降低对处理器资源的占用,CMS 曾引入增量式并发收集器(i-CMS),通过让用户线程与 GC 线程交替运行,减少垃圾收集线程对系统资源的独占。但实践表明 i-CMS 的效果一般,从 JDK 7 开始已被声明为废弃,并在 JDK 9 中完全移除。


七、Garbage First(G1)收集器

7.1 概述

Garbage First(简称 G1)收集器是垃圾收集技术发展中的一个里程碑,标志着基于局部收集设计思路和 Region 内存布局的引入。G1 的设计目标是能够有效管理大内存的 Java 服务端应用,它逐步取代了 CMS 收集器,并在 JDK 9 之后成为默认垃圾收集器。G1 的核心目标是提供一个具有可预测停顿时间的垃圾收集器,这对于需要高可用性和低延迟的应用场景至关重要。

7.2 G1 的发展历史

  • 早期历史
    G1 从 JDK 7 开始作为一个实验性收集器存在,直到 JDK 7 Update 4 才被认为具备商用价值,移除了“Experimental”标识。

  • JDK 9 以后
    G1 收集器逐步取代了 Parallel Scavenge + Parallel Old 组合,成为服务端模式下的默认垃圾收集器,而 CMS 被标记为不推荐使用(Deprecate)。

7.3 G1 的核心特点

  • 可预测的停顿时间模型
    G1 通过允许用户指定最大停顿时间(使用 -XX:MaxGCPauseMillis 参数),在回收过程中动态选择回收价值最高的 Region,从而尽可能保证垃圾收集停顿时间在预定范围内。

  • Region 内存布局

    • Region:每个 Region 具有不同的回收策略,可以高效回收不同种类的对象。

    • Humongous 区域:当对象大小超过单个 Region 容量的一半时,G1 会将其分配到多个连续的 Region,这些区域通常被视为老年代的一部分。

  • G1 将堆内存划分为多个大小相等的独立 Region,每个 Region 可充当新生代(Eden 或 Survivor)或老年代区域。

  • 混合回收模式(Mixed GC)
    在 G1 中,不同的 Region 可以根据需求选择参与回收,不再局限于新生代或老年代。通过选择垃圾量最多、回收效果最佳的 Region 进行回收,G1 能更高效地管理内存并保持应用稳定。

7.4 G1 的回收阶段

G1 的垃圾收集过程主要分为以下四个阶段:

  1. 初始标记(Initial Marking)
    仅标记 GC Roots 可直接引用的对象,并设置 TAMS(Top at Mark Start)指针。此阶段停顿时间非常短,通常与 Minor GC 同步完成。

  2. 并发标记(Concurrent Marking)
    从 GC Roots 开始,递归扫描堆中所有对象,标记存活对象。此阶段耗时较长,但可以与用户线程并发执行。在此过程中,G1 采用了 SATB(Snapshot-at-the-Beginning)算法来记录并发期间对象引用的变化,确保标记信息的准确性。

  3. 最终标记(Final Marking)
    在并发标记结束后,对因用户线程活动而发生变动的对象进行短暂停顿处理,确保所有对象都已正确标记。

  4. 筛选回收(Evacuation)
    根据每个 Region 的统计数据(包括回收耗时、垃圾量等),选择回收价值最高的 Region 作为回收集,将存活对象复制到空闲 Region 中,并清理旧 Region 空间。此阶段需要暂停用户线程,由多线程并行执行对象复制任务。

7.5 停顿时间预测模型

G1 的设计目标之一是满足用户指定的最大停顿时间。为此,G1 会统计每个 Region 的回收时间,并采用**衰减均值(Decaying Average)**方法计算回收价值。

  • 衰减均值:通过赋予最近的数据更高的权重,衰减均值能更准确地反映当前 Region 的回收成本。基于这些数据,G1 能够预测若干 Region 回收所需的总停顿时间,并在不超过 -XX:MaxGCPauseMillis 参数设置的时间限制下,动态选择最优的回收集(Collection Set)。

7.6 G1 收集器的优缺点

优点
  • 可预测的停顿时间
    用户可以通过 -XX:MaxGCPauseMillis 指定期望的最大停顿时间,G1 根据回收集的优先级进行区域回收,以达到预期的停顿目标。

  • 高吞吐量与低延迟的平衡
    G1 能在延迟可控的情况下尽可能提高吞吐量,适用于大规模服务端应用。

  • 灵活的内存管理
    动态调整 Region 大小和回收策略,使得内存回收更灵活、效率更高,同时避免了全堆扫描带来的长时间停顿。

缺点
  • 内存占用较高
    G1 为每个 Region 都维护记忆集(通过卡表实现),因此额外内存开销可能占用整个堆容量的 10% 至 20%。

  • 写屏障负载较大
    为了实现 SATB 算法跟踪并发期间的引用变化,G1 除了使用写后屏障外,还需要写前屏障。这一复杂操作增加了运行时的计算负担。

  • 设计复杂性
    基于 Region 的内存布局、回收集选择和停顿预测模型等机制使得 G1 的实现非常复杂,调优参数众多,使用不当可能导致性能下降。

7.7 关键挑战
  • 跨 Region 引用问题
    由于堆内存被划分为多个 Region,必须处理不同 Region 之间的引用关系。G1 通过记忆集(Card Table)实现“双向”卡表结构,记录哪些 Region 指向当前 Region 以及当前 Region 被哪些 Region 引用,从而在垃圾收集时仅扫描必要的区域。

  • 用户线程与垃圾收集的干扰
    在并发标记阶段,为防止用户线程修改对象图结构导致标记错误,G1 使用 SATB(Snapshot-at-the-Beginning)算法记录引用变化,确保最终标记结果的准确性。

  • Full GC 的风险
    如果垃圾收集速度无法跟上内存分配速度,可能会触发 Full GC,从而导致长时间的“Stop The World”。因此,G1 必须精确控制内存回收速度,确保垃圾收集与内存分配的平衡。

7.8 性能调优建议
  • 停顿时间设置
    通过 -XX:MaxGCPauseMillis 设置合理的停顿时间(通常在 200 到 300 毫秒之间),确保停顿时间与回收效率的平衡。停顿时间设置过低可能导致回收集规模太小,从而无法跟上内存分配速度,最终引发 Full GC。

  • 内存配置
    合理配置堆内存大小、Region 大小(通过 -XX:G1HeapRegionSize 设置)以及其他 JVM 参数,有助于进一步优化 G1 收集器的性能。

G1 收集器是现代垃圾收集器设计的重要里程碑,通过灵活的内存布局、可预测的停顿时间和高效的回收策略,适应了现代服务端应用对垃圾收集器的高要求。

相关文章:

  • 鸿蒙项目源码-天气预报app-原创!原创!原创!
  • 通过Appium理解MCP架构
  • 基于龙芯3A5000处理器,全国产标准6U VPX板卡解决方案
  • RHCSA项目实践
  • Vite社区模板的使用
  • Ubuntu下编译PX4原生飞控固件
  • vue 两种路由模式
  • 【算法笔记】一维前缀和
  • 二维量子力学:使用Python求解非平凡势
  • 【算法应用】基于粒子群算法PSO求解无人机集群路径规划问题
  • 一个用 C 语言打印出所有三位数水仙花数的程序
  • 蓝桥杯备考------>二维差分板子题之地毯
  • Spring Web MVC(Spring MVC)
  • OGG故障指南:OGG-01163 Bad column length (xxx) specified for column
  • zookeeper详细介绍以及使用
  • 什么是 OLAP 数据库?企业如何选择适合自己的分析工具
  • 软链接解决docker中的conda路径错误:ModuleNotFoundError: No module named ‘Cpython‘
  • 「MethodArgumentTypeMismatchException:前端传递 ‘undefined‘ 导致 Integer 类型转换失败」
  • 向量数据库是什么,它有什么作用?
  • 【计网】数据包
  • 科室建设网站/湖南网站推广优化
  • 政务网站开发协议/东莞seo排名收费
  • 随州网站建站/软文营销成功案例
  • 拉萨网站建设价格/宁波网络营销推广公司
  • 知乐商城是什么网站/外链推广是什么意思
  • qq推广文案怎么写/seo软件资源