GC 概念/必要性/早期回收行为
GC 是什么
GC(Garbage Collection,垃圾回收)是 JVM 自动回收不再使用的对象、释放内存的机制。它让 Java 程序员从手动内存管理中解放出来——不需要 malloc / free,也不需要 new / delete。
为什么要 GC
C/C++ 的内存噩梦
在 C/C++ 中,内存需要程序员手动管理:
// C 语言手动内存管理
void process() {
char* buffer = (char*)malloc(1024); // 分配
// ... 使用 buffer ...
free(buffer); // 释放
}手动管理的问题:
| 问题 | 说明 | 后果 |
|---|---|---|
| 忘记释放 | 分配后忘记 free | 内存泄漏(Memory Leak) |
| 重复释放 | free 两次同一个指针 | 未定义行为,崩溃 |
| 使用已释放内存 | free 后继续访问 | Use-After-Free,安全漏洞 |
| 野指针 | 释放后指针未置空 | 悬挂指针(Dangling Pointer) |
这就是为什么 C/C++ 程序经常出现内存相关的问题——delete 少了会泄漏,delete 多了会崩溃。
Java 的解决方案
Java 通过 GC 彻底解决了这个问题:
public class GCAuto {
public static void main(String[] args) {
// 分配对象——完全自动
Object obj = new Object();
// 使用对象——完全自动
// 无需手动释放
// GC 会自动在合适的时机回收 obj
// 无论是否忘记释放,GC 都能兜底
}
}GC 的核心价值
| 价值 | 说明 |
|---|---|
| 防止内存泄漏 | 不再使用的对象自动回收 |
| 防止悬挂指针 | 对象引用自动置 null,访问时报 NPE |
| 提高开发效率 | 程序员不需要关心内存分配和释放 |
| 提高安全性 | 避免了 Use-After-Free 等内存安全问题 |
GC 的代价
GC 不是免费的。它带来的问题是停顿时间(Pause Time)——GC 时需要暂停所有应用线程,直到垃圾回收完成。
public class GCPause {
public static void main(String[] args) {
// GC 发生时的停顿,对用户是透明的
// 但如果停顿时间过长,就会影响响应
while (true) {
process();
}
}
}停顿时间的实际表现
| GC 类型 | 停顿时间 | 用户感知 |
|---|---|---|
| MinorGC | 几毫秒~几十毫秒 | 通常无感知 |
| FullGC | 几十毫秒~几秒 | 明显卡顿 |
| SerialGC | 最长停顿 | 停顿感明显 |
| G1/ZGC | 亚毫秒~毫秒 | 基本无感知 |
停顿时间是评价 GC 最重要的指标之一。
GC 的历史演变
早期:手动追踪
1990 年代,GC 的研究比 Java 更早,但效率很低:
- 引用计数(Reference Counting):无法处理循环引用
- 追踪式 GC(Tracing GC):需要 stop-the-world
1995:Java 引入分代 GC
Java 1.0 开创性地引入了分代回收策略,利用「大多数对象朝生夕灭」的经验观察,大幅提升了 GC 效率。
堆分区:
年轻代(Young Gen)
Eden + Survivor(S0/S1)
老年代(Old/Tenured Gen)2002:HotSpot 发布
Sun 发布了 HotSpot JVM,带来了分代 GC 的成熟实现:
- Serial GC(单线程)
- Throughput GC(并行,年轻代 + 老年代并行)
2006:CMS 并发 GC
Java 6 引入 CMS(Concurrent Mark Sweep),第一次实现了并发 GC——GC 线程和应用线程同时运行,大幅减少停顿时间。
2012:G1 发布
JDK 7 正式发布 G1(Garbage-First) GC。G1 不再使用传统的分代结构,而是把堆划分为多个大小相等的 Region,实现可预测的停顿时间。
2018+:ZGC 和 Shenandoah
JDK 11 引入 ZGC,JDK 12 引入 Shenandoah,实现了亚毫秒级停顿的 GC,GC 停顿时间从秒级降到了毫秒以下。
GC 的时机
GC 不是随时发生的。JVM 根据内存使用情况自动触发:
public class GCTrigger {
public static void main(String[] args) {
// 场景一:Eden 区满 → MinorGC
for (int i = 0; i < 1_000_000; i++) {
byte[] data = new byte[1024]; // 不断分配,Eden 区满触发 MinorGC
}
// 场景二:老年代满 → FullGC
// 大量对象晋升老年代,老年代满触发 FullGC
// 场景三:显式调用
System.gc(); // 建议执行 GC,不保证立即执行
}
}JVM 的早期回收行为(演进史)
JDK 7 之前:Serial GC 独占
# 默认使用 Serial GC(单线程 Stop-The-World)
java -server MyApp
# 或
java -client MyApp # 客户端模式,Serial GCJDK 7:G1 加入
G1 作为服务端 GC 的备选方案加入,但默认仍使用 Throughput GC(Parallel GC)。
JDK 9:G1 成为默认 GC
JDK 9 开始,G1 成为默认的垃圾回收器。
# JDK 9+ 默认使用 G1
java -version
# OpenJDK 17 默认使用 G1JDK 21:ZGC 成为默认 GC
JDK 21 LTS 开始,ZGC 成为默认 GC。
# JDK 21+ 默认 ZGC
# 亚毫秒级停顿时间,GC 几乎对应用无感知GC 的基本流程
无论哪种 GC 收集器,都遵循以下基本流程:
1. 标记(Mark)
找出所有存活对象(GC Roots 可达的对象)
2. 清除(Sweep)
回收所有未被标记的对象
3. 压缩(Compact,可选)
把存活对象移到一起,消除内存碎片本节小结
GC 的核心要点:
| 关键点 | 说明 |
|---|---|
| 是什么 | JVM 自动回收不再使用的对象、释放内存的机制 |
| 为什么需要 | 防止内存泄漏、悬挂指针,提高开发效率和安全性 |
| 代价 | GC 停顿时间(Stop-The-World) |
| 历史 | 从 Serial → Parallel → CMS → G1 → ZGC |
| JDK 9+ 默认 | G1 |
| JDK 21+ 默认 | ZGC |
理解 GC 的必要性和历史演进,是学习 GC 算法和回收器的基础。
下一节,我们来看 垃圾回收算法(核心)。
