JVM - 主流垃圾收集器

JVM 解密 —— 主流垃圾收集器

垃圾收集器是垃圾收集算法的具体实现。在介绍它们之前,必须先理解一个核心概念:“Stop-The-World” (STW)

  • 什么是 STW?:在垃圾收集器运行期间,会强制暂停所有正在执行的用户线程。从用户的角度看,程序就像“卡住”或“冻结”了。这个过程被称为“Stop-The-World”。
  • 停顿时间 (Pause Time):就是指 STW 的持续时间。停顿时间是衡量一款垃圾收集器性能的重要指标。对于需要快速响应的应用(如网站、API服务),过长的停顿是不可接受的。

不同的收集器有不同的特点,适用于不同的业务场景。它们通常分为两大类:新生代收集器老年代收集器


1. 新生代收集器

新生代的特点是对象“朝生夕死”,存活对象少。因此,新生代收集器通常采用标记-复制算法,追求高效率。

1.1 Serial 收集器

  • 特点: 一个单线程工作的收集器。在它进行垃圾收集时,会触发 STW,暂停所有用户线程。
  • 优点: 实现简单高效。对于单核 CPU 环境,由于没有线程交互的开销,它甚至是效率最高的。
  • 缺点: STW 带来的用户体验差。
  • 适用场景: 主要用于客户端模式(Client Mode)下的虚拟机,比如个人桌面应用。

生活化比喻:一个人的保洁

  • 团队构成: 只有一个保洁员。
  • 工作方式: 当办公室需要打扫时,他会通过广播通知:“全体员工请注意,现在开始清洁,请大家立刻放下手中的工作,原地待命!”(STW)。然后,他一个人推着小车,从一楼到顶楼,独自完成所有的清洁工作。在此期间,所有员工都只能干等着,直到他宣布清洁完毕。
  • 特点: 因为只有一个保洁员,且所有人都必须等他,所以停工时间(STW)可能会比较长。这就像 Serial 收集器的单线程工作模式。

1.2 ParNew 收集器

  • 特点: 是 Serial 收集器的多线程版本。它同样会 STW,但在收集期间会使用多个线程并行地进行垃圾回收。
  • 优点: 在多核 CPU 环境下,能有效利用多核优势,比 Serial 更快,缩短 STW 时间。
  • 重要地位: 在 JDK 7 之前,它是与老年代的 CMS 收集器配合使用的首选新生代收集器。

生活化比喻:团队协作保洁

  • 团队构成: 一支由多名保洁员组成的团队。
  • 工作方式: 清洁开始时,保洁经理同样会通过广播通知:“全体员工请注意,现在开始清洁,请大家立刻放下手中的工作,原地待命!”(STW)。但这次,整个保洁团队会一拥而上,每人负责一个区域,同时进行清洁。
  • 特点: 因为人多力量大,清洁工作很快就完成了。虽然员工们同样需要停工等待,但总的等待时间(STW)比只有一个人打扫时要短得多。这就像 ParNew 收集器的多线程并行工作模式。

1.3 Parallel Scavenge 收集器

  • 特点: 类似于 ParNew,也是一个多线程、使用复制算法的新生代收集器。但它的关注点不同。
  • 核心目标: 它的目标是达到一个可控制的吞吐量 (Throughput)。吞吐量 = CPU 用于运行用户代码的时间 / (CPU 运行用户代码时间 + CPU 垃圾收集时间)。高吞吐量意味着 CPU 能高效地运行用户代码,但单次 GC 的停顿时间可能较长。
  • 自适应调节: 提供了 -XX:MaxGCPauseMillis(最大垃圾收集停顿时间)和 -XX:GCTimeRatio(吞吐量大小)等参数,虚拟机会根据这些设置动态调整堆的各项参数。
  • 适用场景: 适合在后台运算而不需要太多交互的任务,追求高吞吐量,高效地完成计算。

生活化比喻:关注总工时的保洁团队

  • 团队构成: 一支同样由多人组成的、但以“效率”为最高目标的保洁团队。
  • 工作方式: 这个团队的关注点与众不同。他们不关心单次清洁打扰了员工多久,而是关心“员工总有效工作时间”。保洁经理的目标是让“员工工作时间 / (工作时间 + 清洁时间)”这个比值最大化(高吞吐量)。
  • 特点: 他们可能会选择在垃圾堆积得最多的时候,进行一次集中、高效的大扫除。虽然这一次停工等待的时间可能不短,但通过减少清洁频率,保证了员工在一天内的总工作时间最长,整个公司的“产出”最高。这就像 Parallel Scavenge 收集器关注吞吐量的特点。

2. 老年代收集器

老年代的特点是对象存活率高,空间大。因此,老年代收集器通常采用标记-清除标记-整理算法。

2.1 Serial Old 收集器

  • 特点: 是 Serial 收集器的老年代版本,同样是单线程的,使用标记-整理算法。

生活化比喻:独自整理仓库的管理员

  • 工作场景: 这个保洁员现在负责打扫公司仓库(老年代),这里存放的都是长期使用的、重要的物资。
  • 工作方式: 他仍然是一个人工作,并且要求所有领用物资的员工全部停下(STW)。在仓库里,他先标记出所有还需要使用的物资,然后把它们全部搬到仓库的一侧,紧凑地码放整齐(标记-整理)。最后,把另一侧所有的垃圾和空地一次性清扫干净。
  • 特点: 独自搬运和整理所有物资非常耗时,但好处是整理后的仓库留出了一大片连续的空地,方便存放新的大件物资。

2.2 Parallel Old 收集器

  • 特点: 是 Parallel Scavenge 收集器的老年代版本,是多线程的,使用标记-整理算法。
  • 组合: 在注重吞吐量的场景下,它与 Parallel Scavenge 收集器是“黄金搭档”。

生活化比喻:团队协作整理仓库

  • 工作场景: 这次是那支“关注总工时”的保洁团队来打扫公司仓库(老年代)。
  • 工作方式: 经理通知所有员工停工(STW)。随后,整个团队的成员一起动手,标记出所有重要物资,然后分工合作,将这些物资快速地搬到仓库的一侧并码放整齐(标记-整理)。
  • 特点: 因为是团队协作,整理仓库的速度比一个人快得多,员工停工等待的时间也相应缩短了。这支团队与他们在办公室(新生代)的“高吞吐量”风格保持一致。

2.3 CMS (Concurrent Mark Sweep) 收集器

  • 特点: 是一款以获取最短回收停顿时间(Low Pause Time,即低停顿时间)为目标的收集器。它在垃圾收集的大部分阶段,可以与用户线程并发执行,从而极大地缩短 STW 的时间。
  • 算法: 基于标记-清除算法。

生活化比喻:在不打断派对的情况下打扫房间

想象一下,你正在家里开一个热闹的派对(用户线程在运行),但房间开始变得越来越乱(内存垃圾增多)。你既想把房间打扫干净,又不想因为打扫而长时间中断派对,让客人们干等着。CMS 就是你雇佣的那个聪明的保洁团队。

这个保洁团队是这样工作的:

  1. 初始标记 (Initial Mark) -> 第一次短暂暂停

    • 保洁员的操作:保洁团队负责人拍拍手,大喊一声:“所有人别动,一秒钟就好!”(Stop-The-World)。然后,他用眼睛快速扫一下,把你(GC Root)和你正拿在手里的杯子、手机(GC Root 直接关联的对象)贴上“重要物品”的标签。这个过程非常快,客人们几乎没感觉到。
    • CMS 的行为: 短暂暂停所有用户线程,仅标记出 GC Roots 能直接找到的对象。速度极快。
  2. 并发标记 (Concurrent Mark) -> 并行工作,不打扰

    • 保洁员的操作: 客人们继续唱歌、聊天、玩游戏(用户线程恢复运行)。保洁团队的成员们则悄悄地在人群中穿梭,查看哪些东西是和那些“重要物品”关联的(比如,手机充电器连着手机,所以充电器也是重要的),也给它们贴上“重要物品”的标签。这个过程耗时最长,但因为是和派对同时进行的,所以客人们完全无感
    • CMS 的行为: 与用户线程并发执行。从“初始标记”出的对象开始,遍历整个对象图,找出所有存活的对象。
  3. 重新标记 (Remark) -> 第二次短暂暂停

    • 保洁员的操作: 在保洁员标记期间,有的客人可能放下了旧杯子,换了一杯新的。为了不错过这些变化,负责人再次拍手大喊:“大家再停一下,马上好!”(Stop-The-World)。团队成员快速根据刚才客人的新动作,修正之前的标记(比如把旧杯子上的标签撕掉,给新杯子贴上)。这个过程比第一次稍长,但也很快。
    • CMS 的行为: 再次短暂暂停所有用户线程,修正并发标记期间因用户操作而产生变动的对象标记。
  4. 并发清除 (Concurrent Sweep) -> 并行清理

    • 保洁员的操作: 客人们再次恢复活动(用户线程恢复运行)。保洁团队拿着垃圾袋,把所有没有贴“重要物品”标签的垃圾(比如空瓶子、废纸巾)都清理掉。这个过程也是和派对同时进行的,客人们也无感
    • CMS 的行为: 与用户线程并发执行,回收所有未被标记的垃圾对象。

这个例子的关键点:

  1. 低停顿: 整个打扫过程中,派对只被短暂地打断了两次,耗时最长的“找东西”和“扔垃圾”环节都是在派对进行中完成的。这就是 CMS 低停顿的核心优势。

  2. 内存碎片(CMS 的缺点): 保洁员只是把垃圾从原地拿走,并没有移动那些“重要物品”。结果是,房间里虽然干净了,但到处都是零散的、不连续的空地。如果这时想搬一个长沙发(分配一个大对象)进来,可能会因为找不到足够大的连续空间而失败。这就是 CMS 基于“标记-清除”算法带来的内存碎片问题。

  • 执行过程:
    1. 初始标记 (Initial Mark): (STW) 仅标记 GC Roots 能直接关联到的对象,速度很快。
    2. 并发标记 (Concurrent Mark): (并发) 与用户线程一起执行,进行可达性分析,找出所有存活对象。
    3. 重新标记 (Remark): (STW) 修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,时间比初始标记稍长。
    4. 并发清除 (Concurrent Sweep): (并发) 与用户线程一起执行,清除未标记的垃圾对象。
  • 优点: 并发收集、低停顿。由于最耗时的并发标记和并发清除阶段都可以和用户线程一起工作,所以它带来的 STW 时间非常短,对用户响应影响小。
  • 缺点:
    1. 产生内存碎片: 基于“标记-清除”,会产生大量空间碎片。
    2. 对 CPU 资源敏感: 会占用一部分 CPU 资源,导致用户程序变慢。
    3. 浮动垃圾: 在并发清除阶段,用户线程新产生的垃圾无法被本次 GC 回收,只能留待下次,称为“浮动垃圾”。

3. G1 (Garbage-First) 收集器:划时代的统一收集器

G1 是一款面向服务端应用的垃圾收集器,从 JDK 9 开始成为默认的垃圾收集器。它开创了化整为零的思路,不再物理上区分新生代和老年代。

  • 核心思想: 将整个 Java 堆划分为多个大小相等的独立的区域(Region,即区域)。每个 Region 都可以根据需要扮演 Eden、Survivor 或 Old 的角色。G1 会跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region(这也就是 “Garbage-First” (垃圾优先) 的由来)。

生活化比喻:高科技智能保洁团队

  • 工作场景: 这是一支截然不同的高科技保洁团队,他们接管了一座全新的、模块化设计的智能办公大楼。

  • 办公室设计 (Region): 整座大楼没有固定的“新人区”和“资深员工区”,而是被划分成几百个大小完全一样、功能灵活的“智能工作间” (Region)。每个工作间都可以随时切换角色,变成普通办公区、会议室或储藏室。

  • 工作方式:

    1. 数据驱动: 每个智能工作间都装有传感器,实时上报“垃圾指数”(垃圾数量、清理耗时等)。保洁经理的平板上有一个“垃圾价值排行榜”,垃圾最多、最容易清理的工作间排在最前面。
    2. 可预测停顿: 总经理下达指令:“我们只有5分钟的清洁窗口,不能多一秒!”(-XX:MaxGCPauseMillis)。
    3. 优先清理: 保洁经理看着排行榜,对团队说:“时间有限,我们只打扫排行榜上最有价值的那几个房间!”(Garbage-First)。
    4. 高效腾挪: 团队冲进目标房间,把里面仅有的几个重要物品(存活对象)快速搬运到一个备用的空房间里,然后将整个旧房间彻底清空并消毒(局部上的“标记-复制”和“标记-整理”)。
  • 特点: 他们从不试图一次性打扫整栋大楼。他们的目标是在固定的时间内,完成最大价值的清理,并且由于是整体腾挪,所以绝不会产生零散的垃圾死角(无内存碎片)。这使得他们的清洁工作既高效,又对员工的影响(停顿)可控。

  • 优点:

    1. 可预测的停顿模型: 允许用户指定期望的最大停顿时间(-XX:MaxGCPauseMillis)。
    2. 空间整合: 从整体上看是基于“标记-整理”算法,从局部(两个 Region 之间)看是基于“标记-复制”算法,所以不会产生内存碎片
    3. 统一收集: 同时管理新生代和老年代,不再需要与其他收集器配合。
  • 最新发展: ZGC 和 Shenandoah 是目前最新的、以追求极致低停顿为目标的收集器,它们的设计思想更为复杂,通常用于对停顿时间有极端要求的超大内存应用。

posted @ 2026-01-21 16:07  我是刘瘦瘦  阅读(0)  评论(0)    收藏  举报