深入解析 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 收集器的回收过程分为四个阶段:
-
初始标记(Initial Mark)
标记 GC Roots 直接关联的对象。此阶段非常快,需要“Stop The World”,通常与 Minor GC 同步完成。 -
并发标记(Concurrent Mark)
从初始标记的对象开始,遍历整个对象图进行并发标记,不暂停用户线程,但持续记录引用变化(使用写屏障)。 -
重新标记(Remark)
对并发标记结束后仍未标记完全的对象进行修正,此阶段需要短暂暂停用户线程,以确保一致性。 -
并发清除(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 的垃圾收集过程主要分为以下四个阶段:
-
初始标记(Initial Marking)
仅标记 GC Roots 可直接引用的对象,并设置 TAMS(Top at Mark Start)指针。此阶段停顿时间非常短,通常与 Minor GC 同步完成。 -
并发标记(Concurrent Marking)
从 GC Roots 开始,递归扫描堆中所有对象,标记存活对象。此阶段耗时较长,但可以与用户线程并发执行。在此过程中,G1 采用了 SATB(Snapshot-at-the-Beginning)算法来记录并发期间对象引用的变化,确保标记信息的准确性。 -
最终标记(Final Marking)
在并发标记结束后,对因用户线程活动而发生变动的对象进行短暂停顿处理,确保所有对象都已正确标记。 -
筛选回收(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 收集器是现代垃圾收集器设计的重要里程碑,通过灵活的内存布局、可预测的停顿时间和高效的回收策略,适应了现代服务端应用对垃圾收集器的高要求。