This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
# 垃圾回收基础
|
||||
|
||||
!!! note "垃圾回收是编程语言运行时自动管理内存的机制,核心思想:找出程序不再使用的内存并释放。"
|
||||
|
||||
---
|
||||
|
||||
## 什么是垃圾回收
|
||||
|
||||
垃圾回收(Garbage Collection,简称 GC)是编程语言运行时(Runtime)自动管理内存的一种机制。基本判断逻辑:
|
||||
|
||||
- **引用计数** — 每个对象记录有多少东西引用它,归零就回收。Python 用这种。简单高效,但解决不了循环引用。
|
||||
- **可达性分析** — 从根对象出发,遍历能走到的所有对象,走不到的就是垃圾。Java、Go、JavaScript 都用这个。
|
||||
|
||||
## 没有 GC 的语言
|
||||
|
||||
没有垃圾回收的语言,内存管理靠程序员手动完成:
|
||||
|
||||
### 手动管理
|
||||
|
||||
| 语言 | 方式 |
|
||||
|------|------|
|
||||
| **C** | `malloc` / `free` |
|
||||
| **C++** | `new` / `delete`,以及 RAII(作用域结束自动析构) |
|
||||
| **Assembly** | 直接操作内存,没有抽象层 |
|
||||
| **Fortran** | 传统上手动管理 |
|
||||
|
||||
### 所有权 / 借用检查(零成本自动回收,但不是 GC)
|
||||
|
||||
| 语言 | 机制 |
|
||||
|------|------|
|
||||
| **Rust** | 所有权 + 值离开作用域自动释放,编译期检查,无运行时开销 |
|
||||
| **Zig** | 手动 alloc/free,但提供了 allocator 抽象 |
|
||||
|
||||
### 其他方式
|
||||
|
||||
- **D** — 可选 GC,也支持手动管理
|
||||
- **Swift** — ARC(引用计数),严格来说不算传统 GC
|
||||
- **Objective-C / Swift** — ARC 机制
|
||||
|
||||
### 对比理解
|
||||
|
||||
```
|
||||
GC 语言(Java、Go、Python)
|
||||
→ 运行时有个"管家"定期打扫
|
||||
→ 方便,但有 STW 延迟和内存开销
|
||||
|
||||
手动管理(C、C++)
|
||||
→ 自己打扫,扫不干净(泄漏)或扫过头(野指针)都可能出问题
|
||||
|
||||
所有权系统(Rust)
|
||||
→ 编译期就帮你规划好谁负责清理
|
||||
→ 零运行时开销,但学习曲线陡
|
||||
```
|
||||
|
||||
## GC 的代价
|
||||
|
||||
垃圾回收并非免费午餐,主要代价体现在:
|
||||
|
||||
### 1. Stop-The-World(STW)暂停
|
||||
|
||||
GC 在某些阶段必须暂停程序执行,所有业务线程停住,等 GC 干完活。
|
||||
|
||||
- 短暂停(几毫秒)对普通应用无所谓
|
||||
- 但在高频交易、游戏渲染、实时系统里,几毫秒的抖动就是灾难
|
||||
- Java 的 ZGC、Go 的并发 GC 都在拼命缩短这个暂停时间
|
||||
|
||||
### 2. 额外内存开销
|
||||
|
||||
GC 需要额外空间来做标记和跟踪:
|
||||
|
||||
- **引用计数**:每个对象多一个计数器
|
||||
- **三色标记**:需要额外的标记位图或栈
|
||||
- **分代收集**:需要维护多个内存区域和存活/死亡对象的拷贝
|
||||
- **写屏障(Write Barrier)**:每次对象引用变更都要额外记录
|
||||
|
||||
实际经验:**开销大约在 5%~20% 的额外内存**。
|
||||
|
||||
### 3. CPU 时间消耗
|
||||
|
||||
GC 线程本身要吃 CPU:
|
||||
|
||||
- 遍历对象图做可达性分析
|
||||
- 拷贝存活对象
|
||||
- 整理碎片
|
||||
- 这些时间本来可以用来跑业务逻辑
|
||||
|
||||
Go 的并发 GC 把大部分工作放到后台线程做,但还是要占 CPU。
|
||||
|
||||
### 4. 吞吐量 vs 延迟的矛盾
|
||||
|
||||
- **追求高吞吐**:少触发 GC,攒多了一次性清 → 单次暂停更长
|
||||
- **追求低延迟**:频繁触发 GC → 暂停短但总 CPU 开销更高
|
||||
|
||||
这是 GC 调优里永恒的权衡:
|
||||
|
||||
| 收集器 | 侧重 | 特点 |
|
||||
|--------|------|------|
|
||||
| Parallel GC | 高吞吐 | 暂停长,适合后台批处理 |
|
||||
| ZGC | 超低延迟 | 暂停 <1ms,吞吐略低 |
|
||||
| G1 | 折中 | 可预测暂停 |
|
||||
|
||||
### 5. 内存碎片
|
||||
|
||||
不同 GC 策略表现不同:
|
||||
|
||||
| 策略 | 碎片情况 |
|
||||
|------|----------|
|
||||
| 标记-清除 | 有碎片,长时间运行后分配变慢 |
|
||||
| 标记-压缩 | 无碎片,但压缩过程要移动对象,暂停更长 |
|
||||
| 复制算法 | 无碎片,但浪费一半内存空间 |
|
||||
|
||||
碎片多了之后,本来有足够总空闲内存,却分配不出连续的大对象。
|
||||
|
||||
### 6. 不可预测性
|
||||
|
||||
你无法精确控制何时释放:
|
||||
|
||||
```python
|
||||
# 你以为这里释放了
|
||||
obj = None
|
||||
# 但 GC 可能根本不急着回收
|
||||
# 文件句柄、数据库连接这种稀缺资源就悬着
|
||||
```
|
||||
|
||||
所以 GC 语言里经常需要 `try-with-resources`、`with` 语句、析构器配合等机制来手动管理资源释放时机。
|
||||
|
||||
### 7. 维护复杂度
|
||||
|
||||
GC 的实现本身很复杂——分代阈值怎么设?并发标记和业务线程怎么协调?大对象走哪个分配路径?调优参数一堆,出了问题排查困难。
|
||||
|
||||
---
|
||||
|
||||
## 一句话总结
|
||||
|
||||
> **GC 用 CPU、内存、延迟三样东西,换程序员的脑子。** 值不值取决于场景——绝大多数业务应用完全值得;极端性能敏感的场景(嵌入式、高频交易、游戏引擎核心)就可能不值得。
|
||||
|
||||
## 练习题
|
||||
|
||||
??? question "题目一:哪些语言有 GC?哪些没有?"
|
||||
??? success "答案"
|
||||
有 GC:Java、Go、Python、JavaScript、C#、Ruby、Kotlin、Swift(ARC)
|
||||
|
||||
无 GC(手动管理):C、C++
|
||||
|
||||
无 GC(所有权系统):Rust
|
||||
|
||||
可选 GC:D、Swift
|
||||
|
||||
??? question "题目二:引用计数和可达性分析有什么区别?各有什么优缺点?"
|
||||
??? success "答案"
|
||||
引用计数:每个对象维护引用计数器,归零即回收。优点是实现简单、回收及时;缺点是无法处理循环引用(A→B→A),且计数器维护有性能开销。Python 用这种。
|
||||
|
||||
可达性分析:从 GC Root 出发遍历对象图,不可达即为垃圾。优点是能处理循环引用;缺点是遍历耗时,可能需要 STW。Java、Go、JavaScript 用这种。
|
||||
|
||||
## 相关链接
|
||||
|
||||
- [维基百科:Garbage collection (computer science)](https://en.wikipedia.org/wiki/Garbage_collection_(computer_science))
|
||||
- [深入理解 Java 虚拟机 — 周志明](https://book.douban.com/subject/34907498/)
|
||||
@@ -0,0 +1,246 @@
|
||||
# GC 回收器对比
|
||||
|
||||
!!! note "从 Serial 到 ZGC,各回收器的核心差异在于吞吐量与延迟的取舍;G1 取代 CMS 成为主流,因为解决了碎片和可预测暂停问题。"
|
||||
|
||||
---
|
||||
|
||||
## 一句话比喻
|
||||
|
||||
把 Java 堆想象成一栋写字楼:
|
||||
|
||||
```
|
||||
🏢 Serial GC = 一个保安自己扫
|
||||
🏢 Parallel GC = 一群保安一起扫
|
||||
🏢 CMS GC = 保安边上班边偷偷扫
|
||||
🏢 G1 GC = 按楼层分区,哪脏扫哪
|
||||
🏢 ZGC GC = 超级保安,几乎感觉不到
|
||||
```
|
||||
|
||||
## 各回收器定位
|
||||
|
||||
| 回收器 | JDK 版本 | 作用范围 | 核心特点 |
|
||||
|--------|---------|---------|---------|
|
||||
| **Serial** | 1.0+ | 全堆 | 单线程,STW,最简单 |
|
||||
| **Parallel** | 1.2+ | 全堆 | 多线程 STW,追求吞吐量 |
|
||||
| **ParNew** | 1.4+ | 年轻代 | 多线程版 Serial,配合 CMS 用 |
|
||||
| **CMS** | 1.5+ | 老年代 | 尽量并发,追求低延迟 |
|
||||
| **G1** | 1.7+ | 全堆 | 分 Region,可预测暂停 |
|
||||
| **ZGC** | 15+ | 全堆 | 暂停 <1ms,超大堆 |
|
||||
| **Shenandoah** | 12+ | 全堆 | 并发压缩,低延迟 |
|
||||
|
||||
---
|
||||
|
||||
## CMS 回收器(Concurrent Mark Sweep)
|
||||
|
||||
**核心思想:老年代的垃圾,尽量让 GC 线程和业务线程同时跑。**
|
||||
|
||||
### 四个阶段
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant B as 业务线程
|
||||
participant G as GC 线程
|
||||
|
||||
Note over B: 正常运行
|
||||
B->>B: 正常运行
|
||||
|
||||
Note over G: ① 初始标记(极短 STW)
|
||||
G->>G: 标记 GC Roots 直接引用
|
||||
|
||||
Note over B,G: ② 并发标记(不暂停业务)
|
||||
B->>B: 正常运行
|
||||
G->>G: 遍历对象图(耗时最长)
|
||||
|
||||
Note over G: ③ 重新标记(短 STW)
|
||||
G->>G: 修正并发阶段变动
|
||||
|
||||
Note over B,G: ④ 并发清除(不暂停业务)
|
||||
B->>B: 正常运行
|
||||
G->>G: 清除标记为垃圾的对象
|
||||
```
|
||||
|
||||
### 每个阶段详解
|
||||
|
||||
#### ① 初始标记(Initial Mark)⏱ 极短,STW
|
||||
|
||||
只标记 GC Roots 直接引用的对象,不做全图遍历,所以很快。
|
||||
|
||||
#### ② 并发标记(Concurrent Mark)⏱ 最长,但不暂停业务
|
||||
|
||||
从 GC Root 出发,遍历整个对象图。GC 线程和业务线程同时运行。⚠️ 这阶段业务可能产生新的垃圾,标记不到。
|
||||
|
||||
#### ③ 重新标记(Remark)⏱ 短,STW
|
||||
|
||||
修正并发阶段产生的变动,用了 Incremental Update 算法。
|
||||
|
||||
#### ④ 并发清除(Concurrent Sweep)⏱ 长,但不暂停业务
|
||||
|
||||
清除标记为垃圾的对象,GC 线程和业务线程同时运行。
|
||||
|
||||
### CMS 的四个致命问题
|
||||
|
||||
#### 问题 1:浮动垃圾(Floating Garbage)
|
||||
|
||||
并发阶段产生的新垃圾,重新标记阶段才发现不了,只能等下次 GC 才能回收。需要保守预留更多老年代空间。
|
||||
|
||||
#### 问题 2:内存碎片
|
||||
|
||||
CMS 用的是标记-清除,不压缩。久了之后老年代全是碎片:
|
||||
|
||||
```
|
||||
┌──┬░░┬──┬░░┬──┬░░┬░░┬──┬░░┬──┐
|
||||
│▓▓│ │▓▓│ │▓▓│ │ │▓▓│ │▓▓│
|
||||
└──┴░░┴──┴░░┴──┴░░┴░░┴──┴░░┴──┘
|
||||
```
|
||||
|
||||
想分配一个大对象?放不下 → 触发 Full GC(退化为 Serial 的标记-压缩)。**这是最致命的。**
|
||||
|
||||
#### 问题 3:Concurrent Mode Failure
|
||||
|
||||
GC 还没清完,老年代就满了 → 被迫 STW 做一次 Full GC → 延迟暴增,CMS 的初衷白费了。
|
||||
|
||||
#### 问题 4:CPU 敏感
|
||||
|
||||
并发阶段抢占 CPU 资源。默认 GC 线程数 = (CPU 核数 + 3) / 4。4 核机器上 GC 吃掉 25% CPU。
|
||||
|
||||
---
|
||||
|
||||
## G1 回收器(Garbage-First)
|
||||
|
||||
**核心思想:把堆切成很多小 Region,哪里垃圾最多先扫哪里。**
|
||||
|
||||
### 堆的结构
|
||||
|
||||
传统 GC 把堆分成连续的年轻代/老年代,G1 则把堆切成 Region:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph 传统布局
|
||||
A[Young Gen] --- B[Old Gen]
|
||||
end
|
||||
subgraph G1 布局
|
||||
C[Region 1: E] --- D[Region 2: E] --- E[Region 3: S] --- F[Region 4: O] --- G[Region 5: H] --- H[Region 6: free]
|
||||
end
|
||||
```
|
||||
|
||||
```
|
||||
G1 布局示例(每个 Region 1~32MB,堆被切成约 2048 个):
|
||||
┌────┬────┬────┬────┬────┬────┬────┬────┐
|
||||
│ E │ E │ E │ S │ O │ O │ H │free│
|
||||
├────┼────┼────┼────┼────┼────┼────┼────┤
|
||||
│ O │free│ E │ E │ O │free│ O │ O │
|
||||
├────┼────┼────┼────┼────┼────┼────┼────┤
|
||||
│free│ E │ O │free│free│ S │ E │free│
|
||||
└────┴────┴────┴────┴────┴────┴────┴────┘
|
||||
|
||||
E = Eden S = Survivor O = Old H = Humongous(大对象) free = 空闲
|
||||
```
|
||||
|
||||
### G1 的回收策略
|
||||
|
||||
"Garbage-First" = 优先回收垃圾最多的 Region。
|
||||
|
||||
每个 Region 的角色是**动态分配**的:
|
||||
|
||||
```
|
||||
Region #5 角色变化:
|
||||
│
|
||||
├─→ 这轮是 Eden ← 新对象分配在这里
|
||||
├─→ 下轮变成 free ← GC 后清空
|
||||
├─→ 再下轮变成 Old ← 有对象晋升过来
|
||||
└─→ 之后变成 free ← 老年代 GC 后释放
|
||||
```
|
||||
|
||||
### G1 的核心优势:可预测暂停
|
||||
|
||||
```
|
||||
调优命令:-XX:MaxGCPauseTime=200ms
|
||||
|
||||
G1 自动计算:"我有 200ms 时间,哪些 Region 垃圾最多又能在 200ms 内清完?"
|
||||
→ 挑一组 Region 收割
|
||||
→ 保证暂停在 200ms 以内
|
||||
|
||||
时间轴:
|
||||
══╪════════╪═══════╪═══════╪═════════
|
||||
180ms 150ms 160ms 170ms ← 每次都在目标内
|
||||
```
|
||||
|
||||
### G1 的工作阶段
|
||||
|
||||
| 阶段 | 描述 | STW |
|
||||
|------|------|-----|
|
||||
| 年轻代 GC | Eden 满 → 存活对象复制到 Survivor Region | ✅ 短 |
|
||||
| 初始标记 | 标记 GC Roots(搭年轻代 GC 的便车) | ✅ 短 |
|
||||
| 并发标记 | 和业务线程并行遍历对象图 | ❌ 不暂停 |
|
||||
| 重新标记 | 用 SATB 修正并发阶段变动 | ✅ 短 |
|
||||
| 清除 | 清理空 Region,为 Mixed GC 做准备 | ✅ 短 |
|
||||
| Mixed GC | 回收年轻代 + 选一批垃圾多的老年代 Region | ✅ 可控 |
|
||||
|
||||
### G1 动态调整代大小
|
||||
|
||||
```
|
||||
默认:
|
||||
-XX:G1NewSizePercent=5 年轻代最小 5%
|
||||
-XX:G1MaxNewSizePercent=60 年轻代最大 60%
|
||||
|
||||
场景 1:大量短命对象 → G1 自动扩大年轻代
|
||||
┌──────────────────────────────────┐
|
||||
│ E E E E E E E E E E E │ O O O O │
|
||||
│ 年轻代 (80%) │老年代20% │
|
||||
└──────────────────────────────────┘
|
||||
|
||||
场景 2:大量长命对象 → G1 自动缩小年轻代
|
||||
┌──────────────┬──────────────────────────┐
|
||||
│ E E E │ O O O O O O O O O O O O O O O O│
|
||||
│ 10% │ 老年代 90% │
|
||||
└───────┴────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CMS vs G1 正面对比
|
||||
|
||||
| 维度 | CMS | G1 |
|
||||
|------|-----|-----|
|
||||
| 作用范围 | 仅老年代 | 全堆(年轻代 + 老年代) |
|
||||
| 算法 | 标记-清除(不压缩) | 标记-复制 + 标记-压缩 |
|
||||
| 内存碎片 | ❌ 有,严重 | ✅ 无(Region 间整理) |
|
||||
| 暂停时间 | 不可控 | 可预测(设目标值) |
|
||||
| 浮动垃圾 | 有,需预留空间 | 有,但 Mixed GC 兜底 |
|
||||
| 大堆表现 | 差(碎片 + 并发慢) | 好(Region 分治) |
|
||||
| 小堆表现 | 还行 | 开销略高(Region 管理) |
|
||||
| 并发阶段 | 并发标记 + 并发清除 | 并发标记(清除是 STW) |
|
||||
| JDK 默认 | JDK 8 可选 | **JDK 9+ 默认** |
|
||||
|
||||
### 为什么 CMS 被 G1 取代
|
||||
|
||||
```
|
||||
CMS 的致命伤:
|
||||
1. 碎片 → 大对象分配失败 → Full GC → 延迟暴增
|
||||
2. Concurrent Mode Failure → Full GC → 延迟暴增
|
||||
3. 不压缩 → 这两个问题无解
|
||||
4. JDK 9 开始标记为废弃
|
||||
|
||||
G1 的解决方式:
|
||||
1. Region 本身就是压缩的 → 无碎片
|
||||
2. Mixed GC 主动回收老年代 → 不会等到老年代爆满
|
||||
3. 可控暂停 → 不会出现意外的长暂停
|
||||
4. JDK 9+ 默认 GC,生产环境广泛验证
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 练习题
|
||||
|
||||
??? question "题目一:CMS 的"标记-清除"有什么缺点?为什么 G1 要改成"标记-压缩"?"
|
||||
??? success "答案"
|
||||
标记-清除不移动对象,释放后留下碎片。长时间运行后老年代全是碎片,大对象分配失败,被迫触发 Full GC。G1 在 Region 级别做标记-压缩,存活对象复制到新 Region,旧 Region 直接回收,天然无碎片。
|
||||
|
||||
??? question "题目二:G1 的"可预测暂停"是怎么实现的?"
|
||||
??? success "答案"
|
||||
G1 在并发标记结束后,会计算每个 Region 的垃圾比例和回收收益。然后根据用户设定的目标暂停时间(-XX:MaxGCPauseTime),贪心地选择一组 Region 进行 Mixed GC,确保选中的 Region 能在目标时间内清完。
|
||||
|
||||
## 相关链接
|
||||
|
||||
- [Oracle 官方文档 — G1 Garbage Collector](https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc.html)
|
||||
- [Oracle 官方文档 — Concurrent Mark Sweep (CMS) Collector](https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/cms.html)
|
||||
@@ -0,0 +1,155 @@
|
||||
# Java 分代 GC
|
||||
|
||||
!!! note "分代 GC 的核心思想:不同生命周期的对象用不同策略回收——短命的快扫,长寿的慢扫。"
|
||||
|
||||
---
|
||||
|
||||
## 为什么要分代
|
||||
|
||||
研究表明,绝大多数对象都是"朝生夕灭"的(比如方法里的临时变量、请求处理的中间对象),只有少数对象能活很久。分代 GC 就是利用这个规律。
|
||||
|
||||
---
|
||||
|
||||
## 堆的分代结构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Java 堆(Heap) │
|
||||
│ │
|
||||
│ ┌──────────────────────────┐ ┌─────────────────────┐ │
|
||||
│ │ 年轻代 (Young Gen) │ │ 老年代 (Old Gen) │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ ┌─────┬─────┬────────┐ │ │ │ │
|
||||
│ │ │ Eden │ S0 │ S1 │ │ │ │ │
|
||||
│ │ │ 80% │ 10% │ 10% │ │ │ Tenured Space │ │
|
||||
│ │ └─────┴─────┴────────┘ │ │ (70%) │ │
|
||||
│ │ (默认 1/3 堆) │ │ (默认 2/3 堆) │ │
|
||||
│ └──────────────────────────┘ └─────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────┐ │
|
||||
│ │ 元空间 (Metaspace) │ │
|
||||
│ │ 存类的元数据(JDK 8+ 移到堆外,不归 GC 管) │ │
|
||||
│ └──────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 年轻代(Young Generation)—— 细分三块
|
||||
|
||||
### Eden 区(80%)
|
||||
|
||||
新对象都在这里出生。绝大多数对象第一次 GC 就死了。
|
||||
|
||||
### Survivor 0 / Survivor 1(各 10%)
|
||||
|
||||
也叫 S0、S1,交替使用。活过一次 GC 的对象先搬到这里暂住。
|
||||
|
||||
### 运作流程
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[新对象分配到 Eden] --> B{Eden 满了?}
|
||||
B -->|否| A
|
||||
B -->|是| C[触发 Minor GC]
|
||||
C --> D[扫描 Eden + S0,标记存活]
|
||||
D --> E[存活对象复制到 S1]
|
||||
E --> F[Eden 和 S0 全部清空]
|
||||
F --> G{对象年龄 ≥ 阈值?}
|
||||
G -->|否| H[留在 Survivor,年龄+1]
|
||||
G -->|是| I[晋升到老年代]
|
||||
H --> A
|
||||
```
|
||||
|
||||
流程演示:
|
||||
|
||||
```
|
||||
第 1 步:新对象分配到 Eden
|
||||
┌────────┬──────┬──────┐
|
||||
│ Eden │ S0 │ S1 │
|
||||
│▓▓▓▓▓▓▓│ │ │
|
||||
│▓▓▓▓▓▓▓│ │ │
|
||||
└────────┴──────┴──────┘
|
||||
▓ = 新对象
|
||||
|
||||
第 2 步:Eden 满了,触发 Minor GC
|
||||
- 扫描 Eden + S0,标记存活对象
|
||||
- 存活对象复制到 S1
|
||||
- Eden 和 S0 全部清空
|
||||
|
||||
┌────────┬──────┬──────┐
|
||||
│ Eden │ S0 │ S1 │
|
||||
│ │ │▓▓ │
|
||||
└────────┴──────┴──────┘
|
||||
S1 里是活下来的对象
|
||||
|
||||
第 3 步:下次 Minor GC
|
||||
- 扫描 Eden + S1,存活对象复制到 S0
|
||||
- S0 和 Eden 清空
|
||||
- S0 ↔ S1 角色互换
|
||||
|
||||
第 4 步:对象每活过一轮 GC,年龄 +1
|
||||
- 年龄达到阈值(默认 15)→ 晋升到老年代
|
||||
```
|
||||
|
||||
## 老年代(Old Generation)
|
||||
|
||||
存放长期存活的对象:
|
||||
|
||||
- 年龄达标的对象晋升过来
|
||||
- 大对象直接分配在这里(避免在年轻代来回复制)
|
||||
- 回收频率低,用标记-清除或标记-压缩
|
||||
|
||||
## 触发 GC 的条件
|
||||
|
||||
| GC 类型 | 触发时机 | 回收范围 | 算法 | 特点 |
|
||||
|---------|---------|---------|------|------|
|
||||
| Minor GC | Eden 区满 | Eden + 活跃 Survivor | 复制算法 | 快(几毫秒),频繁 |
|
||||
| Major GC / Full GC | 老年代空间不足 | 整个堆(甚至方法区) | 标记-整理/清除 | 慢(几百毫秒~秒级) |
|
||||
| Mixed GC | G1 专用,老年代占阈值 | 年轻代 + 部分老年代 Region | 混合 | 折中,可控暂停 |
|
||||
|
||||
## 对象晋升流程
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[new 对象] --> B[Eden 区]
|
||||
B --> C{Minor GC}
|
||||
C -->|死亡| D[直接回收]
|
||||
C -->|存活| E[进入 Survivor]
|
||||
E --> F{年龄 ≥ 15?}
|
||||
F -->|否| E
|
||||
F -->|是| G[晋升到老年代]
|
||||
G --> H{老年代满?}
|
||||
H -->|否| I[继续存活]
|
||||
H -->|是| J[触发 Major GC / Full GC]
|
||||
```
|
||||
|
||||
## 为什么这样设计有效
|
||||
|
||||
```
|
||||
场景:一个 Web 请求处理
|
||||
├─ 请求进来了,创建一堆临时对象 → Eden
|
||||
├─ 处理完,这些对象没人引用了
|
||||
├─ Minor GC 一扫,99% 都是垃圾,直接清掉
|
||||
└─ 真正活得久的(连接池、缓存)慢慢晋升到老年代
|
||||
|
||||
效率:只扫描 Eden(小区域),速度极快
|
||||
真正长寿的对象不频繁被扫描
|
||||
```
|
||||
|
||||
## GC 收集器演进
|
||||
|
||||
| JDK 版本 | 年轻代收集器 | 老年代收集器 | 默认 |
|
||||
|---------|-------------|-------------|------|
|
||||
| 1.x ~ 7 | Serial / Parallel / ParNew | Serial Old / CMS / Parallel Old | Parallel |
|
||||
| 8 | Parallel | Parallel Old | Parallel |
|
||||
| 9+ | G1(全堆) | G1(全堆) | **G1** |
|
||||
| 15+ | ZGC / Shenandoah | ZGC / Shenandoah | 可选 |
|
||||
|
||||
## 练习题
|
||||
|
||||
??? question "题目一:为什么 Survivor 区要分成 S0 和 S1 两个?"
|
||||
??? success "答案"
|
||||
因为复制算法需要一块空闲空间来存放存活对象。如果只有一个 Survivor 区,就没地方复制了。两个 Survivor 区交替使用:Minor GC 时,把 Eden 和当前 Survivor 中存活的对象复制到另一个 Survivor,然后清空前者。这样每次都有一块干净的空间可用。
|
||||
|
||||
??? question "题目二:对象年龄阈值默认是 15,为什么不能设太小?"
|
||||
??? success "答案"
|
||||
如果阈值太小,很多对象还没来得及死亡就被晋升到老年代,导致老年代快速增长,触发频繁的 Major GC。Major GC 比 Minor GC 慢得多。默认 15 是经验值,大多数短命对象在年轻代就被回收了,少数真正长寿的对象才晋升。年龄存储在对象头的 Mark Word 中,占 4 位,所以最大值就是 15。
|
||||
@@ -0,0 +1,13 @@
|
||||
# JVM 与垃圾回收
|
||||
|
||||
!!! note "深入理解 Java 虚拟机的内存管理机制"
|
||||
|
||||
本章系统整理了垃圾回收(GC)的核心知识,从基础概念到高级算法,涵盖:
|
||||
|
||||
| 文档 | 主题 | 关键词 |
|
||||
|------|------|--------|
|
||||
| [垃圾回收基础](garbage-collection.md) | GC 概念、无 GC 的语言、GC 的代价 | 自动内存管理 |
|
||||
| [内存碎片与整理](memory-fragmentation.md) | 碎片产生原因、标记-清除/压缩/复制三种整理方式 | 碎片、Mark-Sweep、Mark-Compact |
|
||||
| [分代 GC](generational-gc.md) | Java 分代结构、Eden/Survivor/老年代、对象晋升流程 | 分代、Minor GC、Major GC |
|
||||
| [GC 回收器对比](gc-collectors.md) | Serial/Parallel/CMS/G1/ZGC 对比,重点 CMS 与 G1 | CMS、G1、可预测暂停 |
|
||||
| [GC Roots 与三色标记](tri-color-marking.md) | GC Root 类型、可达性分析、三色标记法及并发问题 | GC Root、漏标、SATB |
|
||||
@@ -0,0 +1,159 @@
|
||||
# 内存碎片与整理方式
|
||||
|
||||
!!! note "内存碎片是对象随机分配/释放后产生的空洞,主流 GC 用标记-清除、标记-压缩、复制算法三种方式应对。"
|
||||
|
||||
---
|
||||
|
||||
## 什么是内存碎片
|
||||
|
||||
程序运行时,内存就像一排格子,每次 `new` 对象就占几个格子。释放后留下空洞,如果空洞分散,就会出现**总空闲空间够用,但放不下一个大对象**的情况。
|
||||
|
||||
### 碎片产生过程
|
||||
|
||||
初始分配:
|
||||
|
||||
```
|
||||
┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
|
||||
│A │A │B │B │C │C │C │D │D │D │E │E │F │F │F │G │
|
||||
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
|
||||
```
|
||||
|
||||
释放 B、D、F 后:
|
||||
|
||||
```
|
||||
┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
|
||||
│A │A │░░│░░│C │C │C │░░│░░│░░│E │E │░░│░░│░░│G │
|
||||
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
|
||||
░░ = 空闲碎片
|
||||
```
|
||||
|
||||
总空闲 8 格,但没有一块连续。想分配一个 4 格的对象?**放不下。** 这就是内存碎片。
|
||||
|
||||
---
|
||||
|
||||
## 三种主流整理方式
|
||||
|
||||
### 1. 标记-清除(Mark-Sweep)
|
||||
|
||||
不做整理,只标记死对象然后就地释放。
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[标记阶段] -->|从 GC Root 遍历| B[标记所有存活对象]
|
||||
B --> C[清除阶段]
|
||||
C -->|释放未标记对象| D[就地释放]
|
||||
D --> E[产生碎片]
|
||||
```
|
||||
|
||||
流程演示:
|
||||
|
||||
```
|
||||
标记阶段:标记哪些还活着
|
||||
┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
|
||||
│A │A │B │B │C │C │C │D │D │D │E │E │F │F │F │G │
|
||||
│✓ │✓ │ │ │✓ │✓ │✓ │ │ │ │✓ │✓ │ │ │ │✓ │
|
||||
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
|
||||
|
||||
清除后:
|
||||
┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
|
||||
│A │A │░░│░░│C │C │C │░░│░░│░░│E │E │░░│░░│░░│G │
|
||||
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
|
||||
```
|
||||
|
||||
✅ 速度最快,只扫描一遍
|
||||
❌ **碎片原封不动,碎片越来越多**
|
||||
|
||||
### 2. 标记-压缩(Mark-Compact)
|
||||
|
||||
标记后把所有活对象**挤到一端**,消除空洞。
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[标记阶段] --> B[标记存活对象]
|
||||
B --> C[压缩阶段]
|
||||
C -->|活对象向一端移动| D[消除碎片]
|
||||
D --> E[更新引用]
|
||||
```
|
||||
|
||||
流程演示:
|
||||
|
||||
```
|
||||
标记完成:
|
||||
┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
|
||||
│A │A │░░│░░│C │C │C │░░│░░│░░│E │E │░░│░░│░░│G │
|
||||
│✓ │✓ │ │ │✓ │✓ │✓ │ │ │ │✓ │✓ │ │ │✓ │
|
||||
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
|
||||
|
||||
压缩 — 活对象向左移动:
|
||||
A → 移到 [0]
|
||||
C → 紧跟 A 放到 [2]
|
||||
E → 紧跟 C 放到 [5]
|
||||
G → 紧跟 E 放到 [7]
|
||||
|
||||
整理后:
|
||||
┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
|
||||
│A │A │C │C │C │E │E │G │░░│░░│░░│░░│░░│░░│░░│░░│
|
||||
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
|
||||
已用(紧凑) 空闲(连续一大块)
|
||||
```
|
||||
|
||||
✅ **碎片彻底消除**
|
||||
❌ 要移动对象,更新所有指向它的引用,**暂停时间长**
|
||||
|
||||
### 3. 复制算法(Copying)
|
||||
|
||||
把内存切成两半(From / To),每次只用一半,GC 时把活对象复制到另一半。
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[From 区使用中] --> B[GC 触发]
|
||||
B --> C[扫描存活对象]
|
||||
C --> D[复制到 To 区]
|
||||
D --> E[From/To 角色互换]
|
||||
E --> A
|
||||
```
|
||||
|
||||
流程演示:
|
||||
|
||||
```
|
||||
From 区(当前使用) To 区(空闲备用)
|
||||
┌──┬──┬──┬──┬──┬──┬──┐ ┌──┬──┬──┬──┬──┬──┬──┐
|
||||
│A │A │░░│C │C │░░│E │ │ │ │ │ │ │ │ │
|
||||
│✓ │✓ │ │✓ │✓ │ │✓ │ │ │ │ │ │ │ │ │
|
||||
└──┴──┴──┴──┴──┴──┴──┘ └──┴──┴──┴──┴──┴──┴──┘
|
||||
```
|
||||
|
||||
GC 时,把活对象按顺序复制到 To 区:
|
||||
|
||||
```
|
||||
From 区(废弃) To 区(新的活跃区)
|
||||
┌──┬──┬──┬──┬──┬──┬──┐ ┌──┬──┬──┬──┬──┬──┬──┐
|
||||
│A │A │░░│C │C │░░│E │ │A │A │C │C │E │░░│░░│
|
||||
└──┴──┴──┴──┴──┴──┴──┘ └──┴──┴──┴──┴──┴──┴──┘
|
||||
↑ 全部作废 紧凑排列,后面全是空闲
|
||||
```
|
||||
|
||||
✅ 分配极快(指针一推就行),**天然无碎片**
|
||||
❌ **浪费一半内存**,存活对象多时复制成本高
|
||||
|
||||
---
|
||||
|
||||
## 总结对比
|
||||
|
||||
| 算法 | 速度 | 碎片 | 内存代价 | 适用场景 |
|
||||
|------|------|------|----------|----------|
|
||||
| 标记-清除 | ⚡ 快 | ❌ 有碎片 | 无额外 | CMS 老年代 |
|
||||
| 标记-压缩 | 🐢 慢 | ✅ 无碎片 | 无额外 | G1 老年代 |
|
||||
| 复制算法 | ⚡ 快 | ✅ 无碎片 | 浪费 50% | 年轻代(死亡率高) |
|
||||
|
||||
实际的 GC 实现通常**混合使用**——比如 Java 的分代 GC:年轻代用复制算法(对象死亡率高,复制量小),老年代用标记-压缩(对象存活久,复制成本太高)。
|
||||
|
||||
## 练习题
|
||||
|
||||
??? question "题目一:为什么复制算法通常只用于年轻代?"
|
||||
??? success "答案"
|
||||
因为研究表明绝大多数对象都是"朝生夕灭"的,年轻代中每次 Minor GC 后存活对象通常不到 10%。复制算法只需要复制极少量存活对象,效率很高。而老年代对象存活率高,复制成本太大,所以用标记-压缩。
|
||||
|
||||
??? question "题目二:标记-清除为什么会导致碎片越来越多?"
|
||||
??? success "答案"
|
||||
标记-清除只是就地释放死对象的内存,不会移动任何活对象。随着程序不断分配和释放,空洞越来越分散,连续空闲空间越来越小。大对象可能因为找不到连续空间而触发 Full GC 甚至 OOM。
|
||||
@@ -0,0 +1,203 @@
|
||||
# GC Roots 与三色标记法
|
||||
|
||||
!!! note "GC Roots 是可达性分析的起点,三色标记法是现代低延迟 GC 的核心标记算法,通过写屏障解决并发时的漏标问题。"
|
||||
|
||||
---
|
||||
|
||||
## GC Roots
|
||||
|
||||
GC Root 是垃圾回收的**起点**。从这些对象出发,能走到的就是活的,走不到的就是垃圾。
|
||||
|
||||
### 七种 GC Root
|
||||
|
||||
| 类型 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| 虚拟机栈中的引用 | 每个线程栈帧中的局部变量 | 方法里的 `User user = new User()` |
|
||||
| 静态变量 | 类的 static 引用 | `private static AppConfig instance` |
|
||||
| 常量池引用 | static final 引用的对象 | `private static final String NAME` |
|
||||
| JNI 引用 | 本地方法中持有的 Java 对象 | `native void method(Buffer buf)` |
|
||||
| 活跃线程 | 每个正在运行的线程 | `Thread` 对象 |
|
||||
| 同步锁 | 被 `synchronized` 持有的对象 | `synchronized (sharedLock)` |
|
||||
| Class 对象 | 已加载类对应的 Class 对象 | `User.class` |
|
||||
|
||||
### 可达性分析图
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
R1[虚拟机栈局部变量] --> A[对象 A]
|
||||
A --> B[对象 B]
|
||||
B --> C[对象 C]
|
||||
B --> D[对象 D]
|
||||
|
||||
R2[静态变量] --> E[对象 E]
|
||||
E --> F[对象 F]
|
||||
|
||||
R3[JNI 引用] --> G[对象 G]
|
||||
|
||||
R4[活跃线程] --> H[对象 H]
|
||||
R5[同步锁] --> I[对象 I]
|
||||
R6[Class 对象] --> J[对象 J]
|
||||
|
||||
K[对象 K] -.->|无引用链| R1
|
||||
L[对象 L] -.->|仅被 K 引用| K
|
||||
|
||||
style K fill:#ff6b6b,color:#fff
|
||||
style L fill:#ff6b6b,color:#fff
|
||||
style R1 fill:#51cf66,color:#fff
|
||||
style R2 fill:#51cf66,color:#fff
|
||||
style R3 fill:#51cf66,color:#fff
|
||||
style R4 fill:#51cf66,color:#fff
|
||||
style R5 fill:#51cf66,color:#fff
|
||||
style R6 fill:#51cf66,color:#fff
|
||||
```
|
||||
|
||||
K 和 L 没有任何 GC Root 能到达 → 垃圾,可回收。
|
||||
|
||||
---
|
||||
|
||||
## 三色标记法
|
||||
|
||||
垃圾回收中用来标记对象存活状态的算法,是 G1、ZGC、Go 等现代 GC 的核心。
|
||||
|
||||
### 三种颜色
|
||||
|
||||
| 颜色 | 含义 | 最终状态 |
|
||||
|------|------|---------|
|
||||
| ⚪ 白色 | 还没扫描到的 | 垃圾,可回收 |
|
||||
| 🔘 灰色 | 扫描过了,但它引用的对象还没扫 | 等待继续扫描 |
|
||||
| ⚫ 黑色 | 扫描过了,它引用的对象也都扫了 | 存活 |
|
||||
|
||||
### 完整标记过程
|
||||
|
||||
用一个例子演示:
|
||||
|
||||
```
|
||||
对象关系:
|
||||
GC Roots → A → B → C → G
|
||||
↓ ↓
|
||||
D (G 仅被 C 引用)
|
||||
GC Roots → E → F
|
||||
```
|
||||
|
||||
**初始状态**:GC Root 直接引用的对象标灰,其余全白。
|
||||
|
||||
```
|
||||
⚫ A ⚪ B ⚪ C ⚪ D ⚫ E ⚪ F ⚪ G
|
||||
```
|
||||
|
||||
**扫描 A**:A 有引用 → B,A 变黑,B 变灰。
|
||||
|
||||
```
|
||||
⚫ A 🔘 B ⚪ C ⚪ D ⚫ E ⚪ F ⚪ G
|
||||
```
|
||||
|
||||
**扫描 B**:B 有引用 → C、D,B 变黑,C 和 D 变灰。
|
||||
|
||||
```
|
||||
⚫ A ⚫ B 🔘 C 🔘 D ⚫ E ⚪ F ⚪ G
|
||||
```
|
||||
|
||||
**扫描 C**:C 有引用 → G,C 变黑,G 变灰。
|
||||
|
||||
```
|
||||
⚫ A ⚫ B ⚫ C 🔘 D ⚫ E ⚪ F 🔘 G
|
||||
```
|
||||
|
||||
**扫描 D、F、G**:无更多引用,全部变黑。
|
||||
|
||||
```
|
||||
⚫ A ⚫ B ⚫ C ⚫ D ⚫ E ⚫ F ⚫ G
|
||||
```
|
||||
|
||||
所有对象变黑,标记完成,无白色对象可回收。
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph "① 初始状态"
|
||||
A1[⚫A] --> B1[⚪B]
|
||||
E1[⚫E] --> F1[⚪F]
|
||||
end
|
||||
subgraph "② 扫描 A"
|
||||
A2[⚫A] --> B2[🔘B]
|
||||
E2[⚫E] --> F2[⚪F]
|
||||
end
|
||||
subgraph "③ 扫描 B"
|
||||
A3[⚫A] --> B3[⚫B]
|
||||
B3 --> C3[🔘C]
|
||||
B3 --> D3[🔘D]
|
||||
E3[⚫E] --> F3[⚪F]
|
||||
end
|
||||
```
|
||||
|
||||
### 流程总结
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[GC Root 标灰] --> B[取一个灰色对象]
|
||||
B --> C[扫描它的引用]
|
||||
C --> D[灰变黑,引用的白变灰]
|
||||
D --> E{还有灰色?}
|
||||
E -->|是| B
|
||||
E -->|否| F[剩余白色 = 垃圾]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 并发标记的两大问题
|
||||
|
||||
三色标记通常和业务线程**并发执行**,会产生问题:
|
||||
|
||||
### 问题一:漏标 — 活对象被误判为垃圾 💀
|
||||
|
||||
**同时**满足两个条件才会漏标:
|
||||
|
||||
1. 存在黑色对象到白色对象的**新引用**
|
||||
2. 所有灰色对象到该白色对象的引用都被**删除**
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant GC as GC 线程
|
||||
participant App as 业务线程
|
||||
|
||||
Note over GC: ⚫A 🔘B ⚪C<br>A→C, B→C
|
||||
App->>App: A 删除 A→C
|
||||
App->>App: B 新增 B→C
|
||||
Note over GC: A 是黑色,不会再被扫描<br>C 是白色 → 误判为垃圾!💥
|
||||
```
|
||||
|
||||
### 问题二:多标 — 死对象被当成活的(浮动垃圾)😅
|
||||
|
||||
并发期间某个对象变成垃圾,但标记阶段已经过了它 → 这次回收不了。下次 GC 就能回收,**不致命**,只是多占一轮内存。
|
||||
|
||||
### 解决漏标
|
||||
|
||||
| 方案 | 策略 | 使用者 | 优缺点 |
|
||||
|------|------|--------|--------|
|
||||
| **增量更新** | 黑色新增引用时,把它变回灰色,重新扫描 | CMS | 缺点:Remark 阶段可能较长 |
|
||||
| **SATB**(原始快照) | 删除引用时,记录被删除的对象,标记它 | G1 | 缺点:可能多标浮动垃圾,但不会漏标 |
|
||||
| **染色指针 + 读屏障** | 硬件级别解决 | ZGC | 暂停 <1ms |
|
||||
|
||||
### 对比总结
|
||||
|
||||
| | 传统标记 | 三色标记 |
|
||||
|---|---------|---------|
|
||||
| 状态 | 二元(已访问/未访问) | 三态(白/灰/黑) |
|
||||
| 并发能力 | 无法区分"还没扫到"和"确认是垃圾" | 精确追踪进度,写屏障维护一致性 |
|
||||
| 使用场景 | 单线程 GC | 并发 GC(G1、ZGC、Go) |
|
||||
|
||||
---
|
||||
|
||||
## 练习题
|
||||
|
||||
??? question "题目一:为什么多标(浮动垃圾)不致命,但漏标会导致程序崩溃?"
|
||||
??? success "答案"
|
||||
多标只是把本该回收的对象多留了一轮,下次 GC 就能回收,影响只是多占一点内存。漏标则相反——活对象被当成垃圾回收了,程序还在用这个对象但内存已经被释放,导致空指针、数据损坏甚至 JVM 崩溃。
|
||||
|
||||
??? question "题目二:G1 的 SATB 和 CMS 的增量更新有什么区别?"
|
||||
??? success "答案"
|
||||
增量更新在"新增引用"时拦截:黑色对象新增了对白色对象的引用,就把黑色变灰重新扫描。SATB 在"删除引用"时拦截:灰色对象删除了对白色对象的引用,就把被删除的白色对象记录下来,在标记结束时强制标灰。G1 选 SATB 是因为删除引用更常见(对象字段赋 null),拦截成本更低。
|
||||
|
||||
## 相关链接
|
||||
|
||||
- [深入理解 Java 虚拟机 — 三色标记与写屏障](https://book.douban.com/subject/34907498/)
|
||||
- [Oracle — ZGC: A Scalable Low-Latency Garbage Collector](https://docs.oracle.com/en/java/javase/21/gctuning/z-garbage-collector.html)
|
||||
@@ -120,6 +120,13 @@ nav:
|
||||
- HeavyKeeper: algorithm/heavykeeper.md
|
||||
- 二叉树的遍历: algorithm/binary-tree-traversal.md
|
||||
- 队列: algorithm/queue.md
|
||||
- JVM:
|
||||
- jvm/index.md
|
||||
- 垃圾回收基础: jvm/garbage-collection.md
|
||||
- 内存碎片与整理: jvm/memory-fragmentation.md
|
||||
- 分代 GC: jvm/generational-gc.md
|
||||
- GC 回收器对比: jvm/gc-collectors.md
|
||||
- GC Roots 与三色标记: jvm/tri-color-marking.md
|
||||
- 七牛云:
|
||||
- qiniu-cloud/index.md
|
||||
- 前后端架构: qiniu-cloud/architecture.md
|
||||
|
||||
Reference in New Issue
Block a user