【后端开发|JVM基础01】—— Java垃圾回收全解:从分代内存模型到各收集器的分代职责

Java 垃圾回收全解:从分代内存模型到各收集器的分代职责

很多人问"垃圾收集器到底是收哪一代的垃圾",这个问题的坑在于:它默认了每个收集器都收"一整堆"。实际上 JVM 把堆分成了年轻代和老年代,不同收集器要么只负责其中一代、要么整堆一把抓、要么干脆不分代。搞懂这张地图,比死记每个收集器的名字有用得多。

本文按一条递进链讲透:GC 为什么存在(内存模型)→ GC 在收什么(对象可达性)→ 用什么算法收(三种回收算法)→ 具体哪个收集器收哪一代(核心)→ 怎么搭配怎么选型。


一、先厘清:GC 到底在收"哪一代"的垃圾

1.1 JVM 运行时内存分区

JVM 把运行时内存按用途分了几块。跟垃圾回收直接相关的只有堆(Heap),其他区域要么不参与回收、要么回收规则简单:

区域 是否参与 GC 说明
堆(Heap) ✅ 主要回收对象 存放对象实例,GC 的主战场
方法区 / 元空间(Metaspace) ⚠️ 有回收但较弱 存放类元信息、常量池,JDK8+ 用元空间替代永久代,主要回收"废弃的类和无用的常量"
虚拟机栈 / 本地方法栈 / 程序计数器 ❌ 不参与 GC 栈帧随方法进出栈自动创建销毁

🔴 重点:平时说的"垃圾回收",九成九指堆的回收。

1.2 堆的分代模型

堆内部不是一坨整块,而是按"对象存活时间"切成不同区域:

sql 复制代码
┌────────────────────────────── 堆 ──────────────────────────────┐
│  年轻代(Young)              │          老年代(Old)             │
│  ┌──────────┬─────┬─────┐    │                                 │
│  │  Eden    │ S0  │ S1  │    │                                 │
│  └──────────┴─────┴─────┘    │                                 │
└──────────────────────────────────────────────────────────────┘
   Eden:新对象出生地
   Survivor S0/S1:从 Eden 熬过一轮 GC 的"幸存者",两块来回倒
   Old:熬过多轮 GC 的对象,或者大对象直接进来
  • 年轻代:Eden + 两块 Survivor(S0/S1)。绝大多数对象在这出生、也在这消亡。
  • 老年代:存活久、晋升上来的对象,以及大对象。
  • 分代回收的三种叫法:Young GC(新生代/Minor GC)、Old GC(Major GC,一般特指老年代)、Full GC(整堆回收)。

1.3 为什么要分代:弱分代假说

分代不是拍脑袋,背后是两个经验性规律(合称"分代收集理论"):

  • 弱分代假说(Weak Generational Hypothesis):绝大多数对象都是"朝生夕灭"的,用完就扔。
  • 强分代假说(Strong Generational Hypothesis):熬过越多次回收的对象,越难以消亡。

由此推导出一个设计:把朝生夕灭的对象集中放在年轻代,回收时只处理这一小块、重点保留少量存活对象,而不是每次整堆扫描去标记大量将死的对象。这大幅降低了单次回收的成本。

⚠️ 避坑:分代是"大多数情况"下的优化,不代表老年代对象不会引用年轻代对象。做年轻代回收时,JVM 会借助"记忆集(Remembered Set)"来记录老年代对年轻代的引用,避免整堆扫描老年代。


二、回收的前提:先判定哪些对象是"垃圾"

GC 的第一步不是"收",而是"判断谁该死"。主流判断方式是可达性分析,而不是简单数引用。

2.1 引用计数法的缺陷

早期语言用引用计数:每个对象记录被引用的次数,为 0 就回收。它有两个硬伤,导致 JVM 不用它:

  • 无法处理循环引用(A 引 B、B 引 A,彼此都不为 0,但已无人可达)。
  • 每次引用赋值都要维护计数器,开销大。

2.2 可达性分析与 GC Roots

JVM 采用可达性分析:从一组根节点(GC Roots)出发,沿着引用链往下走,能走到的对象标记为"存活",走不到的即为可回收垃圾。

可作为 GC Roots 的对象主要包括:

GC Root 来源
虚拟机栈(栈帧本地变量表)中引用的对象 正在执行的方法里的局部变量
方法区中静态属性引用的对象 类的静态变量
方法区中常量引用的对象 字符串常量池、字面量等
本地方法栈中 JNI 引用的对象 Native 方法持有的引用
被同步锁(synchronized)持有的对象 锁监视器
活跃线程对象、类加载器等 JVM 内部引用

✅ 一句话:从 GC Roots 出发"摸得到的活着,摸不到的该死"。


三、回收算法:不同代为什么用不同算法

判断完谁该死,下一步是用什么姿势收。三大经典算法各有短板,这正是"哪一代用哪个算法"由来的根源。

3.1 标记-清除(Mark-Sweep)

分两步:标记存活对象,再统一清除未被标记的对象。

  • 优点:实现简单,无需移动对象。
  • 缺点:产生大量内存碎片,导致后续分配大对象时明明有空间却放不下;且标记和清除两个阶段效率都随对象数量下降。

3.2 复制(Copying)

把内存分成两块,只用一块;回收时把存活对象复制到另一块,原块一次性清空。

  • 优点:无碎片,实现简单,只处理存活对象(存活少时极快)。
  • 缺点:浪费空间(总有一半空闲),存活对象多时复制成本高。

所以复制算法天然适合年轻代------因为年轻代绝大多数对象朝生夕灭、存活率低,复制成本低,还能省下整理碎片的时间。JVM 把它细化为 Eden + 两块 Survivor(比例默认 8:1:1),不是简单对半切,避免一半内存闲置。

3.3 标记-整理(Mark-Compact)

标记存活对象后,把它们往一端移动压实,再清掉边界之外的空间。

  • 优点:无碎片,空间利用率高。
  • 缺点:移动对象需要更新所有引用,STW 成本高。

标记-整理适合老年代------老年代对象存活率高、复用复制算法不划算,而长期运行又受不了碎片。

3.4 算法与代的对应关系

算法 空间浪费 碎片 存活率高时表现 适合的代
标记-清除 无 严重 一般 (CMS 用,靠后续整理兜底)
复制 有 无 差 年轻代
标记-整理 无 无 好 老年代

四、各收集器 + 分代职责(核心章节)

🔴 本节回答你最关心的问题:每个收集器到底收哪一代。先记结论------传统收集器分三类:只收年轻代、只收老年代、整堆一把抓;而 G1 用 Region 打破了代边界,ZGC / Shenandoah 干脆不分代。

4.1 只收集【年轻代】的收集器

收集器 算法 线程 特点
Serial 复制 单线程 最简单,适合单核/客户端、内存小的场景
ParNew 复制 多线程 Serial 的多线程版,只能配合 CMS 使用
Parallel Scavenge 复制 多线程 目标是吞吐量优先,可精确控制吞吐量
  • Serial:垃圾回收时"Stop The World"(暂停应用)并单线程收集,简单可靠,是默认兜底。
  • ParNew:本质是 Serial 的并行版。它的存在意义很大一部分是"给 CMS 当年轻代搭档"------CMS 只处理老年代,年轻代得有人收。
  • Parallel Scavenge:关注点在于让吞吐量最大化(CPU 用于用户代码的时间占比),适合后台批量计算场景。

4.2 只收集【老年代】的收集器

收集器 算法 线程 特点
Serial Old(MSC) 标记-整理 单线程 Serial 的老年代版
Parallel Old 标记-整理 多线程 Parallel Scavenge 的搭档,吞吐量优先
CMS 标记-清除 并发 并发低停顿,但会产生碎片
  • Serial Old / Parallel Old:分别是 Serial / Parallel Scavenge 的"老年代另一半",靠标记-整理消灭碎片。
  • CMS(Concurrent Mark Sweep) :追求最短回收停顿 ------标记清除的主要阶段与应用线程并发执行,不全程 STW。代价是标记-清除产生内存碎片 ,且并发阶段占用 CPU。 ⚠️ 避坑:CMS 只收老年代,不碰年轻代 。所以它必须搭配一个年轻代收集器(通常就是 ParNew)。这也是"ParNew + CMS"成为经典组合的原因。 📌 归宿:CMS 在 JDK9 被标记废弃,JDK14(JEP 363)正式移除,由 G1 接棒。

4.3 整堆收集器:G1(打破代边界)

G1(Garbage First) 不再把堆简单切为年轻代/老年代两块,而是把整堆划分成若干固定大小的 Region。每个 Region 可以动态扮演 Eden、Survivor 或 Old 的角色,逻辑上的"年轻代/老年代"只是 Region 集合的概念。

  • 收集目标 :整个堆,但优先回收"垃圾最多"(回收价值最大)的 Region------这就是名字 "Garbage First" 的由来。
  • 特点 :可预测停顿时间(通过 -XX:MaxGCPauseMillis 设定目标),兼顾吞吐与延迟,适合大堆(GB 级以上)。
  • 地位 :JDK9(JEP 248)起成为默认收集器,至今仍是默认。

✅ 一句话:G1 名义上"整堆收集",但内部仍保留年轻代/老年代的分代回收逻辑,只是边界由 Region 动态划定。

4.4 不分代收集器:ZGC / Shenandoah

这两兄弟颠覆了"分代"前提------没有年轻代、老年代的概念,直接整堆回收 。它们追求的是极短甚至接近可忽略的 STW,面向大堆、低延迟场景。

收集器 分代? 核心卖点 演进关键节点
ZGC 最初不分代 停顿时间不随堆大小增长,可到毫秒级 JDK11 引入(实验)→ JDK15 转正 → JDK21 分代化(JEP 439) → JDK23 分代默认 → JDK24 移除非分代模式
Shenandoah 最初不分代 与 ZGC 类似的低延迟,RedHat 开发 JDK12 引入(实验)→ JDK15 转正 → JDK25 分代化(JEP 521)

📌 有意思的演进:"分代"本身是一种优化 。ZGC / Shenandoah 最初为了极简放弃分代,但后来发现年轻对象死得快、频繁回收能省内存和 CPU,于是 JEP 439(ZGC)和 JEP 521(Shenandoah)又都补上了分代能力。这恰好反证了分代假说的价值。

4.5 收集器总表(一句话记忆)

收集器 收集区域 算法 特点 / 备注
Serial 年轻代 复制 单线程,最简
ParNew 年轻代 复制 多线程,配合 CMS
Parallel Scavenge 年轻代 复制 吞吐量优先
Serial Old 老年代 标记-整理 单线程
Parallel Old 老年代 标记-整理 吞吐量优先
CMS 老年代 标记-清除 并发低停顿,JDK14 已移除
G1 整堆(Region) 综合 优先回收垃圾多的块,JDK9 起默认
ZGC 整堆(JDK21 后分代) 综合 超低 STW,大堆
Shenandoah 整堆(JDK25 后分代) 综合 低延迟

五、收集器如何搭配与选型

传统收集器是"一代一个",所以存在经典搭配组合;G1 / ZGC 是整堆的,不需要配。

5.1 经典组合

组合 年轻代 老年代 适用场景
Serial + Serial Old Serial Serial Old 单核、内存小、客户端
ParNew + CMS ParNew CMS 追求低延迟(曾经的主流,CMS 已退役)
Parallel Scavenge + Parallel Old Parallel Scavenge Parallel Old 吞吐量优先,后台批量计算
G1(单收集器) 整堆 Region 整堆 Region JDK9+ 默认,通用
ZGC / Shenandoah(单收集器) 不分代(JDK21/25 后分代) --- 大堆 + 低延迟

5.2 按场景选型(JDK9+ 视角)

  • 没特殊诉求:直接 G1(默认)。
  • 吞吐量优先(能接受停顿):Parallel Scavenge + Parallel Old。
  • 低延迟、堆很大:ZGC 或 Shenandoah。
  • 单核 / 小内存 / 调试:Serial。
  • CMS:生产环境别再用,它是历史概念,JDK14 起已不存在。

5.3 JDK 版本关键变化(容易考的演进史)

版本 变化 依据
JDK 9 G1 成为默认收集器 JEP 248
JDK 11 ZGC 引入(实验) JEP 333
JDK 12 Shenandoah 引入(实验) JEP 189
JDK 14 CMS 被移除 JEP 363
JDK 15 ZGC / Shenandoah 转生产级 JEP 377 / JEP 379
JDK 21 分代 ZGC(Generational ZGC) JEP 439
JDK 23 ZGC 分代模式成为默认 JEP 474
JDK 24 ZGC 移除非分代模式 JEP 490
JDK 25 分代 Shenandoah JEP 521

⚠️ 注意:上面是较新 JDK 的走向。如果你还在用 JDK8,默认仍是 Parallel,也没有 ZGC / Shenandoah(JDK8 无官方版本),升级前要先想清楚收集器的迁移。


六、总结:你真正需要记住的 N 件事

  1. GC 的主战场是堆;堆分年轻代(Eden + 两块 Survivor)和老年代。
  2. 分代的依据是弱分代假说:大多数对象朝生夕灭,集中回收年轻代效率最高。
  3. 判定垃圾用可达性分析:从 GC Roots 出发摸得到的存活,摸不到的回收;不用引用计数(解决不了循环引用)。
  4. 三种算法与代对应:年轻代用复制(无碎片、存活少成本低),老年代用标记-整理(无碎片、能扛高存活),标记-清除有碎片(CMS 的代价)。
  5. 只收年轻代的收集器:Serial、ParNew、Parallel Scavenge。
  6. 只收老年代的收集器:Serial Old、Parallel Old、CMS(CMS 需配 ParNew 收年轻代)。
  7. G1 整堆收集但内部仍分代,靠 Region 动态扮演不同代,JDK9 起是默认。
  8. ZGC / Shenandoah 最初不分代,但 ZGC 在 JDK21、Shenandoah 在 JDK25 都补上了分代------反证分代假说的价值。
  9. 选型:默认 G1;吞吐优先用 Parallel 双开;大堆低延迟用 ZGC / Shenandoah;CMS 已是历史。

验证清单

  • 能不看表说出"每个收集器收哪一代"
  • 能解释"为什么年轻代用复制、老年代用标记-整理"
  • 能说出 CMS 为什么必须搭配 ParNew
  • 能讲清 G1 与"分代"的关系(Region 动态扮演代)
  • 能说清 ZGC 从"不分代"到"分代"的演进(JEP 439/474/490)
  • 能在面试中被问到"默认收集器"时答出"G1(JDK9+)"

参考资源

  • Oracle 官方 GC 调优指南(HotSpot Virtual Machine Garbage Collection Tuning Guide)
  • Oracle《Migrating from JDK 8 to Later JDK Releases》(G1 默认 / JDK 迁移变化)
  • JEP 439: Generational ZGC(OpenJDK)
  • JEP 363: Remove the Concurrent Mark Sweep (CMS) Garbage Collector
  • JEP 248: Make G1 the Default Garbage Collector
  • JEP 474: ZGC: Generational Mode by Default / JEP 490: ZGC: Remove the Non-Generational Mode
  • JEP 521: Generational Shenandoah
  • 《深入理解 Java 虚拟机》(周志明)------分代收集理论、可达性分析、GC Roots 权威参考
相关推荐
geovindu1 小时前
rust: Abstract Factory pattern
开发语言·后端·设计模式·rust·抽象工厂模式
pe7er1 小时前
Spring Boot 日志最佳实践:接入、分级、异常记录与滚动拆分
后端
程序员老赵2 小时前
Docker 部署 Dolibarr:轻松搭建开源 ERP/CRM 平台
运维·前端·后端
量化分析码农3 小时前
【Python量化系统工程实战 #06】从本地脚本到云端部署:量化系统的最小可运行架构
后端
136096757233 小时前
页面打不开不是 Nginx 的错
前端·后端
ALONE阿龙太原微码3 小时前
RBAC 以及主流权限模型
后端
斑鸠喳喳3 小时前
线程本地存储 ThreadLocal
java·后端
你顶住我先撤3 小时前
RocketMQ 消息类型
后端