Skip to content

GC 概念/必要性/早期回收行为

GC 是什么

GC(Garbage Collection,垃圾回收)是 JVM 自动回收不再使用的对象、释放内存的机制。它让 Java 程序员从手动内存管理中解放出来——不需要 malloc / free,也不需要 new / delete

为什么要 GC

C/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 彻底解决了这个问题:

java
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 时需要暂停所有应用线程,直到垃圾回收完成。

java
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 根据内存使用情况自动触发:

java
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 独占

bash
# 默认使用 Serial GC(单线程 Stop-The-World)
java -server MyApp
# 或
java -client MyApp  # 客户端模式,Serial GC

JDK 7:G1 加入

G1 作为服务端 GC 的备选方案加入,但默认仍使用 Throughput GC(Parallel GC)。

JDK 9:G1 成为默认 GC

JDK 9 开始,G1 成为默认的垃圾回收器

bash
# JDK 9+ 默认使用 G1
java -version
# OpenJDK 17 默认使用 G1

JDK 21:ZGC 成为默认 GC

JDK 21 LTS 开始,ZGC 成为默认 GC

bash
# 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 算法和回收器的基础。

下一节,我们来看 垃圾回收算法(核心)

基于 VitePress 构建