一、GC 要解决什么问题
在 C/C++ 中,开发者需要手动管理内存:malloc 分配、free 释放。一旦忘记释放或释放时机错误,就会导致内存泄漏或悬垂指针(dangling pointer)——这是 C/C++ 程序中最难以排查的一类 bug。Java 通过 GC(Garbage Collection,垃圾回收)从根本上解决了这个问题:开发者只管分配,JVM 负责回收。
但”自动”并不意味着”免费”。GC 本身需要消耗 CPU 资源,其运行期间往往会暂停应用线程(Stop-The-World,简称 STW),导致应用出现卡顿。理解 GC 的工作原理,可以帮助我们编写 GC 友好的代码,减少 GC 频率和停顿时间。
本章从 JVM GC 的经典理论出发,深入 ART 的实现细节,系统性地剖析 GC 的方方面面。
二、可达性分析:谁是垃圾?
2.1 引用计数法(为什么 JVM 不用)
引用计数法的思想很简单:每个对象维护一个计数器,记录有多少个引用指向它。计数器为 0 时,对象可以被回收。Python 的 CPython 实现就使用这种方式。
// 引用计数的伪代码 |
引用计数法有两个致命缺陷,导致 JVM/ART 不采用它:
- 循环引用:A 引用 B,B 引用 A,两者计数器永远不为 0。虽然 Python 有辅助的标记-清除来处理循环引用,但 JVM 设计者选择了更彻底的方案。
- 性能开销:每次引用赋值都需要更新计数器。在多线程环境下,还需要原子操作(CAS),性能代价大。
2.2 根搜索算法(GC Roots Tracing)
JVM/ART 使用可达性分析(Reachability Analysis):从一组称为 GC Roots 的根对象出发,沿着引用链追踪,能被追踪到的对象就是”存活”的,其余都是”可回收”的。
GC Roots 包括:
| GC Root 类型 | 说明 | 示例 |
|---|---|---|
| 虚拟机栈中引用的对象 | 当前执行方法的局部变量表 | 方法内的局部变量 Object obj = new Object() |
| 静态变量引用的对象 | 类的 static 字段 | private static final Singleton INSTANCE = ... |
| JNI 全局引用 | Native 代码中的 NewGlobalRef |
JNI 中缓存的全局对象 |
| 活跃线程对象 | 所有存活的 Thread 对象 | Thread 实例本身 |
| 同步监视器对象 | synchronized 持有的对象 |
synchronized(lock) {} 中的 lock |
| ART 内部引用 | Class 对象、ClassLoader、String 常量池 | 类加载时注册到 Runtime 的引用 |
2.3 引用类型与 GC 行为
Java 定义了四种引用类型,它们在 GC 时行为不同:
// 强引用(Strong Reference)—— 永远不会被 GC 回收 |
软引用在 Android 中的应用:Android 的 LruCache 和 DiskLruCache 就是基于强引用 + 内存阈值控制。早期 Android 开发中常用 SoftReference 做图片缓存,但在 ART 中软引用的回收时机较为激进(基本在 GC 时就会被回收),实际上还不如使用 LruCache 手动控制缓存大小来得高效。
三、垃圾回收算法
3.1 标记-清除(Mark-Sweep)
这是最基础也最直观的算法,分为两个阶段:
标记阶段:从 GC Roots 出发,遍历所有可达对象,将它们标记为”存活”。
清除阶段:遍历整个堆,将未被标记的对象回收,并将标记清除以备下次 GC。
优点:无需移动对象,实现简单。 |
标记-清除在 ART 中的应用:ART 的 CMS(Concurrent Mark-Sweep)GC 就是基于此算法,但通过并发标记和清扫阶段来减少 STW 时间。
3.2 标记-整理(Mark-Compact)
在标记-清除的基础上增加了整理阶段:将所有存活对象向堆的一端移动,消除碎片。
优点:消除内存碎片,可以连续分配大对象。 |
适用场景:老年代(Old Generation)中对象存活率高,复制算法效率低,更适合使用标记-整理。
3.3 复制算法(Copying Collection)
将堆分为两个大小相等的半区:From Space 和 To Space。内存只在 From Space 中分配。GC 时将 From Space 中的存活对象复制到 To Space,然后交换两个半区的角色。
Cheney 算法是复制算法的一种高效实现,使用广度优先遍历将对象从 From Space 复制到 To Space,同时修正所有引用。
优点:分配速度快(只需要指针碰撞),无碎片。 |
适用场景:新生代(Young Generation)中大部分对象朝生夕死,存活率低,复制算法效率最高。
3.4 分代回收(Generational Collection)
JVM 将堆划分为新生代(Young Generation)和老年代(Old Generation):
堆内存结构(HotSpot JVM): |
对象分配与晋升流程:
- 新对象在 Eden 区分配(TLAB 机制优化)
- Eden 区满时触发 Minor GC(Young GC):
- 将 Eden 和一个 Survivor 区中的存活对象复制到另一个 Survivor 区
- 清除 Eden 和被复制的 Survivor 区
- 存活对象年龄 +1
- 当对象年龄达到阈值(默认 15,
-XX:MaxTenuringThreshold),晋升到老年代 - 老年代满时触发 Major GC / Full GC(Stop-The-World)
Minor GC vs Major GC vs Full GC: |
四、ART 中的 GC 实现
Android Runtime (ART) 对 GC 做了大量针对移动设备的优化。相关源码位于 AOSP 的 art/runtime/gc/ 目录。
4.1 ART GC 的类型
ART 定义了多种 GC 类型,根据触发原因和回收范围分类:
| GC 类型 | 说明 | 触发条件 |
|---|---|---|
kGcCauseForAlloc |
分配内存时堆空间不足 | 应用分配新对象失败 |
kGcCauseBackground |
后台 GC | ART 判定为合适的时机 |
kGcCauseExplicit |
显式 GC | System.gc() 调用 |
kGcCauseForNativeAlloc |
Native 内存分配压力 | JNI 的 NewGlobalRef 等 |
kGcCauseCollectorTransition |
GC 算法切换 | ART 选择切换 GC 策略 |
源码中的定义在 art/runtime/gc/gc_cause.h:
enum GcCause { |
4.2 CMS(Concurrent Mark-Sweep)—— ART 早期默认 GC
从 Android 5.0 (API 21) 到 Android 9 (API 28),ART 默认使用 CMS(Concurrent Mark-Sweep)GC。CMS 的核心思想是将大部分 GC 工作放在后台线程中并发执行,只在少量关键阶段才需要 STW。
CMS 的各个阶段:
阶段 1: Initial Mark (STW) — 标记 GC Roots 直接可达的对象(暂停 < 1ms) |
CMS 在 ART 中的实现:
- 源码路径:
art/runtime/gc/collector/concurrent_copying.cc - 核心类:
ConcurrentMarkSweep(早期),ConcurrentCopying(Android 10+) - CMS 使用一个 card table 来记录并发标记期间被修改的引用(write barrier)
- 相关机制:Baker Read Barrier(读屏障,用于并发复制 GC)
4.3 CC(Concurrent Copying)—— Android 10 引入
Android 10 (API 29) 引入了 Concurrent Copying (CC) GC,显著改善了 GC 的性能。CC GC 的核心创新:使用 Baker 风格的读屏障(Read Barrier) 实现并发对象复制。
CMS GC 问题: Mark-Sweep 产生内存碎片,长时间运行后需要整理 |
Read Barrier 的工作原理:
当一个对象被 GC 线程移动到新的内存位置后,旧位置会留下一个”转发指针”(forwarding pointer)。应用线程在读取对象引用时,读屏障检查引用是否指向旧位置——如果是,则自动跳转到新位置。
// art/runtime/gc/collector/concurrent_copying.cc (简化逻辑) |
CC GC 的各个阶段:
阶段 1: InitializePhase — 初始化,设置 From/To Space |
4.4 ART GC 的堆结构
ART 的堆与 HotSpot JVM 不同,没有严格的”分代”划分。但 ART 使用 Space 的概念来管理不同的内存区域:
ART 堆结构: |
源码路径:art/runtime/gc/space/ 下的各个 space 实现。
4.5 GC Pause(暂停)与 Android 性能
GC 暂停是导致 Android 应用卡顿的重要原因之一。在 ART 中,GC 的暂停监控可以通过 logcat:
# 查看 GC 日志 |
关键指标解读:
paused 2.543ms:STW 暂停时间(越短越好)total 69.543ms:GC 总耗时(包括并发阶段)25% free:当前空闲堆比例23MB/31MB:已使用 / 总计
五、GC 根与内存泄漏
5.1 常见内存泄漏模式
(1)静态变量持有 Activity 引用
public class MyActivity extends Activity { |
(2)非静态内部类持有外部引用
public class MyActivity extends Activity { |
(3)单例模式持有 Context
public class DataManager { |
5.2 使用 Memory Profiler 检测泄漏
Android Studio 的 Memory Profiler 是检测内存泄漏的利器:
- 打开 Profiler → 选择 Memory
- 反复执行怀疑泄漏的操作(如打开/关闭 Activity)
- 手动触发 GC(点击垃圾桶图标)
- 如果内存曲线呈”锯齿状”上升且不回落,说明有泄漏
- Dump Java heap → 查看 Object 列表 → 搜索怀疑泄漏的类名
5.3 LeakCanary 原理
LeakCanary 是 Square 开源的内存泄漏检测库。其工作原理:
- 监听 Activity/Fragment 的
onDestroy() - 延迟 5 秒后,创建一个弱引用指向已销毁的对象
- 触发 GC
- 检查弱引用是否被清除——如果未被清除,说明存在泄漏
- Dump heap → 分析引用链 → 报告最短泄漏路径
六、避免 GC 性能问题的编程实践
6.1 减少对象分配
// 差:在循环中创建对象 |
6.2 使用 Object Pool
// 使用 Android 内置的对象池 |
6.3 使用合适的集合类
// HashMap vs ArrayMap (Android 特有) |
七、源码路径汇总
| 组件 | 路径 | 说明 |
|---|---|---|
| GC 核心接口 | art/runtime/gc/gc_cause.h |
GC 触发原因枚举 |
| Concurrent Copying GC | art/runtime/gc/collector/concurrent_copying.cc |
CC GC 实现(Android 10+) |
| Mark Sweep GC | art/runtime/gc/collector/mark_sweep.cc |
MS GC 实现 |
| Semi-Space GC | art/runtime/gc/collector/semi_space.cc |
半空间复制 GC |
| Heap | art/runtime/gc/heap.cc |
堆管理主类 |
| Space 实现 | art/runtime/gc/space/ |
各内存空间实现 |
| Baker Read Barrier | art/runtime/gc/collector/concurrent_copying.h |
读屏障声明 |
| GC 性能监控 | art/runtime/gc/accounting/ |
堆统计信息 |
| Reference 处理 | art/runtime/gc/reference_processor.cc |
软/弱/虚引用处理 |
八、常见面试题
Q1: 简述 GC Roots 有哪些?为什么它们能作为根?
A: GC Roots 包括:(1) 当前正在执行的方法的栈帧中的局部变量和参数;(2) 类的静态变量(包括常量池中的引用);(3) JNI 全局引用(GlobalRef);(4) 活跃的 Thread 对象;(5) 被 synchronized 持有的对象(锁对象);(6) ART/JVM 内部的系统类引用(如 ClassLoader、Class 对象)。它们能作为根的共同特征是:这些引用是”活的”——即程序执行所必需、不可能被回收的引用。从这些根出发,沿着引用链走的每一步都证明对象是被需要的。如果没有任何 GC Root 到达某个对象,那么这个对象对程序来说已经”死”了,可以安全回收。
Q2: Minor GC 和 Full GC 有什么区别?什么情况下会触发 Full GC?
A: Minor GC 只回收新生代(Young Gen),发生在 Eden 区满时。由于新生代对象存活率低,Minor GC 通常很快(几毫秒到几十毫秒)。Full GC 回收整个堆(新生代 + 老年代 + 方法区/元空间),通常在以下情况触发:(1) 老年代空间不足;(2) 方法区(Metaspace)空间不足;(3) 调用 System.gc() 显式触发(取决于 JVM 实现);(4) Minor GC 后 Survivor 区放不下存活对象,需要晋升到老年代但老年代也放不下;(5) CMS GC 的 Concurrent Mode Failure(并发回收速度跟不上分配速度)。Full GC 的 STW 时间可达数秒,对应用响应时间影响极大,因此 JVM 调优的核心目标之一就是降低 Full GC 频率。
Q3: ART 的 GC 与 HotSpot JVM 的 GC 有什么不同?为什么 ART 不做严格的分代?
A: 主要区别:(1) ART 使用 Concurrent Copying (CC) GC,HotSpot 使用 G1/Shenandoah/ZGC 等分代或分区 GC;(2) ART 没有严格的”分代”(Eden/Survivor/Old),而是使用 Image Space + Zygote Space + Allocation Space 的划分;(3) 不做严格分代的原因:Android 应用的特点——大部分类在 Zygote fork 时已经预加载(Zygote Space 中的对象是只读的、共享的),应用层新分配的对象生命周期短且量小,因此分代的收益不大。CC GC 通过复制算法已经能很好地处理碎片问题,不需要独立的”老年代整理”。
Q4: 什么是 Write Barrier(写屏障)和 Read Barrier(读屏障)?它们在 GC 中分别起什么作用?
A: Write Barrier 是在对象引用被修改时插入的一段代码,用于通知 GC”这个引用变了”。在 CMS GC 中,并发标记阶段应用线程可能修改已经被标记过的对象的引用,Write Barrier 记录这些修改到 card table(一个记录”脏”内存块的位图),在 Remark 阶段重新扫描。Read Barrier 是在读取对象引用时插入的检查代码。在 CC GC 中,由于对象可能在并发复制阶段被移动,Read Barrier 确保应用线程读取到的始终是对象的最新位置——如果读到的是旧地址(带有转发指针),就自动跳转到新地址。Android 10 的 CC GC 使用 Baker Read Barrier(以发明者 Henry Baker 命名),这是 CC GC 能实现低 STW 的关键技术。
Q5: 如何通过代码降低 GC 压力?给出具体的编程建议。
A: (1) 避免在循环中创建对象——将对象创建移到循环外或使用对象池;(2) 优先使用基本类型(int、long)而非包装类型(Integer、Long),避免自动装箱;(3) 对于小数据量的 Map,使用 ArrayMap 替代 HashMap(Android 平台);(4) 使用 SparseArray 替代 HashMap<Integer, Object>;(5) 及时释放大对象:Bitmap 不用时调用 recycle()(API < 10)或置 null 让 GC 回收;(6) 使用 StringBuilder 而非 + 拼接字符串;(7) 设置合理的缓存上限,避免无限增长(如 LruCache 的 maxSize);(8) 避免在 onDraw() 中分配对象——onDraw() 每帧调用一次,60fps 下就是每秒 60 次;(9) 对于频繁创建销毁的 Activity,注意及时清理静态引用和监听器注册。
Q6: 为什么 System.gc() 不建议频繁调用?ART 是如何处理它的?
A: System.gc() 是一个请求(不是命令),JVM/ART 可以选择忽略它。在 HotSpot JVM 中,-XX:+DisableExplicitGC 可以完全禁用 System.gc()。在 ART 中,System.gc() 通常会导致一次完整的 GC 暂停,浪费 CPU 资源,且回收效果不一定好(因为刚回收完,马上又有新对象分配)。更重要的是,频繁的 Full GC 会导致应用卡顿,用户感知明显。ART 的处理策略:当应用调用 System.gc() 时,如果 ART 认为当前没有 GC 的必要(如堆空间充足),它可能只执行一次轻量级的 GC 甚至不做任何操作。最好的做法是:信任 GC 的自动调度,只在非性能关键路径的特定场景(如进入后台前)调用一次 System.gc() 来建议清理。
参考文档:
- AOSP:
art/runtime/gc/— ART GC 实现 - Oracle: “The Garbage Collection Handbook” (Jones, Hosking, Moss)
- Android Developer: “Manage your app’s memory” https://developer.android.com/topic/performance/memory





