一、为什么要有垃圾回收?
在编程语言发展初期,开发者需要手动管理内存:申请内存空间、使用完毕后手动释放。但这种方式存在两大致命问题:
- 内存泄漏:忘记释放不再使用的内存,导致内存占用持续增长,最终程序崩溃;
- 野指针访问:释放内存后未清空指针,后续误操作该指针会引发程序异常(如崩溃、数据错乱)。
垃圾回收(Garbage Collection,GC)的核心目标是 自动识别并释放 "无用对象" 占用的内存,解放开发者的内存管理负担,同时避免内存泄漏和野指针问题,保障程序稳定性和内存使用效率。简单说:GC 是内存的 "自动清洁工",让开发者专注业务逻辑而非底层内存操作。
二、垃圾回收主要回收哪个内存区域?
以 Java、Python 等主流语言的内存模型为例,GC 的回收重点是 堆内存(Heap),次要关注方法区(元空间 / 永久代),栈内存(虚拟机栈、本地方法栈)通常不参与 GC。
- 堆内存:所有对象实例(如new Object())和数组都存储在堆中,是内存占用最大、对象生命周期最复杂的区域 ------ 有的对象仅使用一次就无用,有的则会持续存活到程序结束。GC 的核心工作就是扫描堆内存,筛选出 "死亡对象" 并回收其空间。
- 方法区:存储类信息、常量、静态变量等,部分场景下也会产生垃圾(如动态卸载类、常量池过期数据),因此部分 GC(如 Java 的 G1、ZGC)会兼顾方法区的回收。
- 栈内存:栈帧随方法调用创建、方法结束销毁,生命周期与线程执行流程强绑定,无需 GC 介入,自动出栈释放。
结论:堆内存是垃圾回收的 "主战场"。
三、标记的过程:如何识别 "垃圾对象"?
GC 的第一步是 "标记"------ 区分 "存活对象" 和 "垃圾对象",核心逻辑是:可达的对象为存活,不可达的为垃圾。主流标记算法有两种:
- 引用计数法(简单但有缺陷)
- 原理:给每个对象维护一个 "引用计数器",被引用时计数器 + 1,引用失效时 - 1;当计数器为 0 时,标记为垃圾。
- 优点:实现简单、标记效率高,实时性强(无需暂停程序)。
- 缺陷:无法解决 "循环引用" 问题(如 A 引用 B,B 引用 A,两者均无其他外部引用,计数器仍为 1,无法被标记回收),因此主流语言(Java、Python)已弃用。
- 可达性分析算法(主流方案)
- 原理:以 "GC Roots" 为起点,遍历对象引用链;能被遍历到的对象为 "可达对象"(存活),遍历不到的为 "不可达对象"(垃圾)。
- GC Roots 的常见类型:
- 虚拟机栈中局部变量表引用的对象(如方法内定义的变量);
- 方法区中静态变量、常量引用的对象;
- 本地方法栈中 Native 方法引用的对象;
- 活跃线程引用的对象。
- 优点:能解决循环引用问题,是 Java、C# 等语言的核心标记算法。
- 注意:标记过程需要 "暂停用户线程"(Stop The World,STW),避免遍历过程中引用关系变化导致标记错误(后续 GC 优化的核心方向之一是减少 STW 时间)。
四、回收的过程:如何释放垃圾对象的内存?
标记完成后,第二步是 "回收"------ 释放垃圾对象占用的内存,同时整理内存碎片(避免碎片过多导致无法分配大对象)。主流回收算法有三种:
- 标记 - 清除算法(基础但低效)
- 流程:① 标记所有垃圾对象;② 直接清除垃圾对象,释放内存。
- 优点:实现简单,无需移动对象。
- 缺陷:
- 产生大量内存碎片(释放的内存块大小不一,分散在堆中);
- 清除效率低(需遍历整个堆查找垃圾对象)。
- 标记 - 复制算法(高效无碎片)
- 流程:① 将堆内存划分为两个大小相等的区域(From 区和 To 区);② 标记存活对象,将其复制到 To 区;③ 清空 From 区,交换 From 和 To 区的角色(下次回收时 From 区变为 To 区,反之)。
- 优点:无内存碎片,回收效率高(只需复制存活对象,存活对象少的时候效率极高)。
- 缺陷:堆内存利用率低(仅一半区域可用),适合 "存活对象少、垃圾多" 的场景(如新生代)。
- 应用:Java 的 Serial GC、ParNew GC 的新生代回收均采用此算法。
- 标记 - 整理算法(适合老年代)
- 流程:① 标记所有存活对象;② 将存活对象向堆的一端移动,集中排列;③ 清除堆另一端的所有垃圾对象,释放连续内存。
- 优点:无内存碎片,堆内存利用率 100%。
- 缺陷:需要移动对象,会增加 STW 时间(移动对象后需更新所有引用该对象的指针)。
- 应用:适合 "存活对象多、垃圾少" 的场景(如老年代),Java 的 CMS GC 老年代、G1 GC 均会用到。
补充:分代收集算法(实际应用的组合方案)
上述三种算法均有优缺点,因此主流 GC(如 Java 的 HotSpot VM)采用 "分代收集"------ 根据对象生命周期长短,将堆内存分为新生代和老年代,分别使用合适的算法:
- 新生代:对象生命周期短(创建后很快成为垃圾),存活对象少 → 用 "标记 - 复制算法"(高效无碎片);
- 老年代:对象生命周期长(长期存活),存活对象多 → 用 "标记 - 清除算法" 或 "标记 - 整理算法"(避免频繁复制的开销)。
五、垃圾回收器的典型实现
垃圾回收器是 GC 算法的具体工程实现,不同语言、不同虚拟机有不同的实现方案,以下是主流语言的典型 GC:
- Java HotSpot VM 的经典垃圾回收器
Java 的 GC 生态最完善,不同回收器针对不同场景优化:
- Serial GC(串行 GC):单线程执行 GC,STW 时间长,适合单 CPU、小型应用(如嵌入式设备),JDK8 默认新生代 GC。
- ParNew GC(并行新生代 GC):Serial GC 的多线程版本,新生代用标记 - 复制,老年代需配合 CMS,适合多 CPU 服务器,缩短 STW 时间。
- CMS GC(并发标记清除):目标是 "低延迟",老年代采用并发标记 + 清除(仅初始标记和重新标记阶段 STW),适合对响应时间敏感的应用(如 Web 服务);缺陷是产生碎片、占用 CPU 资源。
- G1 GC(区域化分代式):JDK9 及以上默认 GC,将堆分为多个大小相等的 Region(区域),兼顾新生代和老年代回收,采用 "标记 - 整理"+"复制" 混合算法,可预测 STW 时间,适合大堆内存(如 10GB+)。
- ZGC/Shenandoah GC(低延迟 GC):专为超大堆、超低延迟设计(STW 时间毫秒级),支持 TB 级内存,适合金融、电商等核心业务。
- Python 的 GC 实现
Python 采用 "引用计数为主,分代回收为辅":
- 日常垃圾回收依赖引用计数,循环引用通过 "分代回收" 解决(将对象分为 3 代,越久未回收的对象扫描频率越低)。
- 优点:兼顾简单性和实用性,避免纯引用计数的循环引用问题。
- Go 的 GC 实现
Go 的 GC 是 "并发标记清除 + 三色标记":
- 无需分代,采用三色标记(白色:未标记;灰色:待遍历;黑色:已标记),配合 "写屏障" 避免并发标记时的引用变化,STW 时间极短(Go 1.19 后可控制在微秒级),适合高并发场景。
六、总结
垃圾回收的核心逻辑可概括为:通过 "标记" 识别无用对象,通过 "回收" 释放内存,通过 "分代 / 并发" 优化性能。其本质是在 "自动化内存管理" 和 "程序性能" 之间寻找平衡 ------ 优秀的 GC 既能解放开发者,又能最小化对程序运行的影响。
不同语言 / 虚拟机的 GC 实现各有侧重(如 Java 追求通用性,Go 追求低延迟,Python 追求简单性),实际开发中需根据应用场景选择合适的 GC 方案(如大内存应用优先 G1/ZGC,低延迟应用优先 CMS/Shenandoah)。