<!-- title: 多数对象刚生就死：垃圾回收如何找回内存 -->
<!-- domain: programming -->
<!-- video: https://www.youtube.com/watch?v=fdHYR-gCTBo -->

# 多数对象刚生就死：垃圾回收如何找回内存

你写过 C/C++ 吗？那种"`malloc` 了就要记得 `free`、`new` 了就要记得 `delete`"的日子，本质上是把一个决定权——**这个内存什么时候可以还回去**——交给了每一个程序员。听起来合理，实践起来是灾难。忘记释放叫内存泄漏（memory leak），程序越跑越胖；释放两次叫双重释放（double-free），内存管理器直接崩溃；释放之后还在用叫释放后使用（use-after-free），这是安全漏洞的温床——曾经震动业界的 Heartbleed，根子就在这里。

那有没有一种办法，让运行时自己决定"哪些内存该回收"？有，这就是**垃圾回收（Garbage Collection, GC）**。SysSketch 这支《What Happens During Garbage Collection?》用动画把这件事拆得清清楚楚：堆是怎么布局的、回收器怎么判断一个对象是"死"是"活"、分代回收为什么快、现代 JVM 又怎么做到几乎不卡顿。它解决的核心问题只有一个——**让开发者彻底忘掉"释放"这件事，同时程序还不会把内存吃光**。这篇文章我带你把它的内核挖到底。

## 视频背景卡片

| 项目 | 内容 |
|------|------|
| 视频标题 | What Happens During Garbage Collection? (Explained Visually) |
| 频道 | SysSketch |
| 主题 | 垃圾回收机制可视化拆解：堆布局、可达性、分代、并发回收 |
| 核心问题 | 运行时如何自动判断并回收"死"对象，又不卡住应用 |
| 适合人群 | 后端工程师、JVM/.NET 用户、面试备考者、想搞懂卡顿根因的人 |

## 核心内容拆解

### 一、"活着"的定义：能不能被找到

视频最关键的一步，是重新定义什么叫"活着"。GC 不去追踪"谁创建了这块内存"，那是引用计数（reference counting）的思路，它有致命缺陷——**循环引用**（A 引用 B，B 引用 A，谁都不会归零）。现代 GC 用的是一个更聪明的判据：**可达性（reachability）**。

它先定义一组**GC Roots（根集合）**，这是回收器眼中的"绝对起点"：

- 栈上的局部变量和方法参数
- CPU 寄存器里的引用
- 全局变量 / 静态字段
- 活跃的 JNI（Java Native Interface）引用

然后规则只有一条：**一个对象"活着"，当且仅当从某个 root 出发，沿着引用链一路能走到它**。走不到的，就是垃圾。这一步的伟大之处在于，它把"该不该释放内存"这个看似需要全局判断的难题，转化成了"从一个起点集合出发遍历一张图"的标准计算机科学问题。这正是 SysSketch 视频里那张从 root 向外辐射的引用图想表达的核心——**死亡不是被宣布的，而是被遗忘的**。（这是视频原话的逻辑内核，下面是我的延伸。）

### 二、Mark-Sweep：标记，然后清扫

判据有了，怎么执行？最朴素的算法叫 **Mark-Sweep（标记-清除）**，分两步：

1. **标记阶段（Mark）**：从所有 roots 出发做深度优先 / 广度优先遍历，每碰到一个可达对象，就在它的对象头里打上一个"已标记"的位。
2. **清扫阶段（Sweep）**：线性扫描整个堆，凡是没被打标记的对象，统统回收。

听起来很美，但有两个硬伤。第一是**内存碎片化（fragmentation）**：死对象东一块西一块地被清掉，留下的空隙大小不一。等你需要一个连续的大对象时，明明总空间够，却塞不进去。第二是遍历要避免递归导致的**栈溢出**——这正是 C.J. Cheney 在 1970 年那篇 ACM 论文要解决的问题（后面讲）。

Mark-Sweep 至今仍在用（比如 Go 的早期设计、很多老式运行时），但它的碎片问题催生了下一个更优雅的方案。

### 三、Copying Collection：复制即整理，分配变免费

为了干掉碎片，人们发明了**复制式回收（Copying Collection）**。思路简单粗暴：把堆劈成两半，叫 **from-space** 和 **to-space**。平时只在 from-space 里分配；回收时，把所有**存活**的对象从 from-space 复制到 to-space，复制的过程中让它们一个挨一个紧凑排列，然后两个空间角色对调。

这一招有四个副产品，每一个都影响深远：

- **碎片自动消失**：存活对象被挤成一坨，天然紧凑。
- **分配变成 O(1)**：在 to-space 里分配新对象，只要把"空闲指针"往前挪一格（bump pointer / 碰撞指针分配），不需要找空隙。**这快得离谱**，是新生代分配极快的根本原因。
- **Cheney's Algorithm**：复制顺序本身就可以充当遍历队列，于是标记+复制可以**不用显式栈**，也不会栈溢出。
- **代价**：浪费一半内存，且如果存活对象很多，复制成本高。

所以复制式回收特别适合**存活率低**的区域——这就直接引出了整个现代 GC 体系的地基。

### 四、分代假说：一切优化的基石

这是 SysSketch 视频里最有"aha"时刻的点，也是我建议你反复琢磨的地方。它叫**分代假说（Generational Hypothesis）**，分弱、强两条：

- **弱分代假说**：绝大多数对象**朝生夕死（die young）**——刚创建没多久就变成垃圾。
- **强分代假说**：一个对象**活得越久，就越可能继续活下去**。

这不是理论推导，是几十年实测的**经验观察**：大量真实程序的统计显示，80%–98% 的对象在第一次回收时就已经死了。临时变量、循环里的中间结果、一次请求里组装的字符串……它们用完即弃。

这条经验直接决定了 GC 的架构。既然"新对象大概率是垃圾"，那把堆**按年龄分代**就划算：

- **新生代（Young Generation）**：放刚创建的对象。频繁回收，但因为大部分是垃圾，回收特别便宜（复制走的存活对象少）。
- **老年代（Old Generation）**：熬过多次新生代回收、被"晋升（promote）"上来的对象。很少回收，因为它们大概率还活着，回收成本高，但频率低。

**这是用"局部性"换"全局效率"的经典操作**——回收器把火力集中在最容易出活、成本最低的地方。Oracle 官方调优文档里那句话几乎是定律："绝大多数对象在新生代里就死掉了。"（视频原话强调了这个分代直觉，下面的数学结论是我的延伸。）

### 五、写屏障与记忆集：跨代引用怎么记账

分代看起来完美，但有个洞：**老年代的对象也可能引用新生代的对象**。如果新生代回收时只从新生代自己的 roots 出发遍历，那些被老年代"指着"的新生代对象就会被误判成垃圾、被错误回收——这是致命的。

怎么补？靠**写屏障（Write Barrier）+ 记忆集（Remembered Set）**。它的逻辑是：每次有代码**往老年代写一个指向新生代的引用**时，触发一段"屏障"代码，把这个老年代区域记进一个叫**卡表（Card Table）**的结构里。于是新生代回收时的 roots = 新生代真正的 roots + 记忆集里指向新生代的那些跨代引用。

这样，**Minor GC（新生代回收）就只需要扫新生代那一小块**，通常几毫秒就结束，完全不用动老年代。这就是 JVM 里"Minor GC 很频繁但很快"的全部秘密。代价是每次写引用都多了一点点开销（写屏障），但跟省下的全堆扫描比，血赚。

### 六、Stop-The-World：你卡顿的真正元凶

到这里你可能会问：回收的时候，应用线程还在跑吗？经典答案是——**必须停下来**。因为回收期间如果应用线程还在改引用关系（比如刚标记完的对象突然没人引用了，或者又冒出新的引用），回收器的遍历结果就会错乱。这个"暂停所有应用线程"的动作叫 **Stop-The-World（STW）**。

STW 就是你感受到的那些莫名其妙卡顿的根源：界面卡一下、接口超时、交易延迟飙升。Minor GC 的 STW 通常很短（毫秒级），但 Full GC（整堆回收）的 STW 可能长达几百毫秒甚至几秒，对延迟敏感的系统（交易、游戏、实时风控）是灾难。

所以过去十几年，JVM 和 .NET 工程师们干的所有事情，几乎都指向同一个目标：**把回收工作从 STW 里搬出来，挪到和应用线程并发执行**。

### 七、低延迟革命：ZGC 与着色指针

视频结尾触及的现代前沿，是**并发收集器（Concurrent Collector）**。代表是 Java 的 G1、ZGC 和 Shenandoah。ZGC 尤其激进，它的设计目标直接写成一行字：**亚毫秒级停顿（sub-millisecond pause）**，且停顿时间不随堆大小增长——10GB 的堆和 10TB 的堆，停顿差不多。

ZGC 的杀手锏是两个，我的延伸解读如下：

1. **着色指针（Colored Pointers）**：64 位指针里，高位其实有不少是空闲的（因为实际用不到那么多地址）。ZGC 把这些"未使用位"拿来存 GC 的元数据——这个对象被标记了吗？需要重定位吗？指针本身就携带了状态。这相当于把记账信息塞进了引用本身。
2. **读屏障（Load Barrier）**：应用线程每次从堆里**读取**一个引用，都会过一道屏障。如果读到的是一个"已被转移 / 需要修复"的着色指针，屏障顺手把它修正成正确的新地址。于是对象的**转移（relocation）可以和应用并发进行**——转移时应用照样跑，只是偶尔在读引用时被屏障"拦截一下"做修正。

Shenandoah 的思路类似，但用 **Brooks 指针**（每个对象多一个指向自己"当前版本"的转发指针）。殊途同归：**让回收和计算真正并行，把 STW 压到几乎不可见**。

## 知识结构图

```text
flowchart TD
    A[内存管理难题] --> B{手动管理<br/>malloc/free}
    A --> C{自动管理<br/>GC}
    B --> B1[双重释放 / 泄漏 / UAF]
    C --> D[可达性判据<br/>GC Roots]
    D --> E[从根遍历引用图<br/>可达=活 不可达=垃圾]
    E --> F[Mark-Sweep<br/>标记后清扫]
    E --> G[Copying<br/>半空间复制+整理]
    G --> G1[副产品: bump pointer<br/>O(1) 分配]
    F --> H[碎片化问题]
    G --> I[适合存活率低的区域]
    I --> J[分代假说<br/>弱:对象朝生夕死<br/>强:越老越长寿]
    J --> K[堆分代<br/>新生代 / 老年代]
    K --> L[Minor GC 极快]
    K --> M[跨代引用问题]
    M --> N[写屏障 + 记忆集<br/>卡表]
    L --> O[Stop-The-World 停顿]
    O --> P[低延迟并发回收<br/>G1 / ZGC / Shenandoah]
    P --> Q[着色指针 + 读屏障<br/>转移与应用并发]
    Q --> R[亚毫秒级停顿]
```

## 实战启示

看完这些机制，对你写代码有什么实际帮助？我给你几条立刻能用上的判断。

**第一，警惕"还可达但已无用"的内存泄漏。** GC 时代的泄漏不是"忘了 free"，而是**对象已经没用了，却还被某个长寿的引用链攥着**——比如塞进一个 `static` 的 Map 当缓存、注册了监听器（listener）却忘了反注册、把对象挂到单例上。回收器判断不了"有没有用"，只认"可不可达"。**只要可达，就永远不会被回收**，这才是 Java/.NET 应用内存泄漏的真正长相。

**第二，少在热路径里造短命大对象。** 弱分代假说说对象朝生夕死，听起来"反正马上回收"无所谓——但回收是有成本的。一个高频循环里疯狂 new 大对象，会撑爆新生代、触发频繁 Minor GC，甚至让大对象直接进老年代（很多 JVM 对超过一定阈值的大对象会绕过新生代直接进 Old，因为复制大对象太贵），最终引发 Full GC。**短循环里复用对象、用基本类型替代包装类、注意 Stream/lambda 捕获带来的隐式分配**，都是这个道理。

**第三，按场景选对 GC。** 吞吐优先（离线计算、批处理）选 G1 或 Parallel；延迟敏感（交易、实时接口、游戏服务端）选 ZGC 或 Shenandoah。JDK 21 起 ZGC 已经是生产可用的低延迟主力，停顿基本压到 1ms 以内。**别再用默认配置硬扛延迟问题**，换一个 GC 往往比优化代码立竿见影。

**第四，理解 STW，就能解释"偶发抖动"。** 当线上出现规律性的延迟毛刺、又查不出业务原因时，先看 GC 日志——很可能就是某次 Full GC 的 STW。会读 GC 日志、能区分 Minor / Mixed / Full，是后端工程师的基本功。

## 延伸学习路径

1. **《The Garbage Collection Handbook》(Jones, Hosking, Moss)** —— GC 领域的圣经，把 Mark-Sweep、Copying、分代、并发全部讲透，看完你比 95% 的开发者懂内存。
2. **Oracle / Microsoft 官方 GC 调优文档** —— Oracle 的 Generations 文档和 .NET 的 GC Fundamentals，权威且免费，是最好的"分代假说"佐证。
3. **OpenJDK ZGC Wiki** —— 着色指针、读屏障、转移并发的官方第一手解释，想真正搞懂 ZGC 看这里。
4. **Cheney's Algorithm (Wikipedia)** —— 1970 年那篇经典算法的精炼解读，理解复制式回收如何免栈。
5. **ByteByteGo EP125《How does Garbage Collection work?》** —— 系统设计视角的 GC 速览，和 SysSketch 的可视化讲解互补。

## 一句话总结

垃圾回收的内核，是把"该不该释放"转化为"从根能不能到达"，再借助"对象朝生夕死"这条经验，用分代 + 写屏障把回收成本压到局部，最终靠并发把停顿压到亚毫秒——你写代码时少造短命大对象、选对 GC，就能让它在你察觉不到的地方安静运转。

## 参考来源

1. [What Happens During Garbage Collection? (Explained Visually) — SysSketch](https://www.youtube.com/watch?v=fdHYR-gCTBo) — 本次深度解读的核心视频
2. [Generations — Oracle Java GC Tuning Guide](https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/generations.html) — 分代假说的官方佐证
3. [ZGC: Sub-Millisecond Pause Garbage Collector Explained](https://prgrmm.com/zgc-sub-millisecond-pause-garbage-collector-explained) — 着色指针与读屏障机制详解
5. [The Generational Hypothesis — Bohrium Sciencepedia](https://www.bohrium.com/en/sciencepedia/feynman/keyword/generational_hypothesis) — 弱/强分代假说的经验基础

---
> 本文由「51学通信」技术团队出品。51学通信由通信行业18年老兵**爱卫生**（微信ID: gprshome201101）维护，聚焦5G/6G核心网、3GPP标准解读、通信前沿技术深度分析。
> 出版作品有《5G核心网原理与实践》《IMS原理与实践》等4本通信专业书籍。知识星球「51学通信」提供深度技术答疑和行业洞察。

> [!tip] 51学通信知识星球
> **星主：爱卫生**（微信：gprshome201101）——公众号「51学通信」创办者，19 年通信行业运维与讲师经验，著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书（各大电商平台有售）。
> **星球定位**：深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域，主打运维岗位与信令分析。
> **当前规模**：5200 篇主题 / 990 个回答，198 元/年。
> **加入福利**：加入星球后请加爱卫生微信，拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题，另有群内分享。
> **AI 加持**：51学通信知识星球已作为 skill 接入 WorkBuddy，使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例，做总结、找文档和 PPT 等场景全覆盖。

![51学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
