【底层机制】Android GC -- 为什么要有GC?GC的核心原理?理解GC的意义
我们将从“为什么需要GC”开始,逐步深入到Android虚拟机中GC的具体实现和最佳实践。
一、 为什么要垃圾回收?
在程序运行时,我们会不断地创建对象(比如在Activity、Fragment中new一个对象),这些对象都占据着内存空间。如果只创建不销毁,内存迟早会被耗尽,导致程序崩溃(OutOfMemoryError)。
在C/C++中,内存需要程序员手动管理(malloc/free, new/delete),这很容易导致两种问题:
- 内存泄漏:忘记释放不再使用的内存。
- 悬空指针:释放了内存后,又去使用它。
GC就是为了解决这个问题而诞生的。它自动追踪和回收不再被使用的对象,释放其占用的内存,从而让程序员从繁琐的内存管理工作中解放出来,专注于业务逻辑。
二、 GC的核心基本原理:可达性分析
GC如何判断一个对象是否“存活”?
核心思想:从一系列被称为 “GC Roots” 的对象出发,向下搜索,所走过的路径称为 “引用链”。如果一个对象到GC Roots没有任何引用链相连,则说明此对象是不可用的,可以被回收。
GC Roots 通常包括以下几种:
- 虚拟机栈中的局部变量:正在执行的各个线程的方法栈帧中的局部变量表所引用的对象。
- 本地方法栈中JNI引用的对象。
- 方法区中静态属性引用的对象:类的静态变量。
- 方法区中常量引用的对象:比如字符串常量池里的引用。
- Java虚拟机内部的引用:如基本数据类型对应的Class对象,常驻的异常对象等。
- 被同步锁持有的对象。
简单来说:如果一个对象,从任何“根”开始都找不到它,它就是垃圾。
三、 Android虚拟机中的GC
Android主要经历了两个虚拟机时代:Dalvik 和 ART。它们的GC机制有显著不同。
1. Dalvik虚拟机(Android 5.0之前)的GC
Dalvik的GC相对简单粗暴,是应用卡顿的一个重要原因。
- 
标记-清除算法 - 标记:暂停所有用户线程(这被称为 “Stop-The-World”),从GC Roots开始,遍历所有存活的对象并打上标记。
- 清除:遍历整个堆,回收所有未被标记的对象所占用的内存。
- 缺点:会产生内存碎片。回收后,内存空间是不连续的,当需要分配一个较大对象时,可能无法找到足够的连续内存,从而触发另一次GC。
 
- 
GC类型: - GC_FOR_MALLOC: 当堆上分配内存失败时触发的GC。
- GC_CONCURRENT: 当堆内存达到一定阈值时,尝试在后台并发执行的GC,以减少应用停顿。
- GC_EXPLICIT: 显式调用 System.gc()触发的GC(强烈不建议,因为它会打乱虚拟机的GC计划)。
 
Dalvik GC的痛点:Stop-The-World 时间较长,尤其是在GC_CONCURRENT的最后阶段,也会暂停所有线程进行清理工作,导致应用卡顿、掉帧。
2. ART虚拟机(Android 5.0及之后)的GC
ART在安装时就将字节码预编译成本地机器码,其GC策略也更加先进和高效,主要目标是减少停顿时间。
- 主要改进:
- 多阶段GC:将GC过程拆分成多个阶段,很多阶段可以与用户线程并发执行。
- 不同种类的GC:针对不同情况使用不同的回收策略。
- 并发标记-清除:大部分标记和清除工作与用户线程并发进行,大大减少了停顿时间。
- 压缩GC:为了解决内存碎片问题,ART会不定期地执行压缩GC。它会移动存活的对象,将它们紧凑地排列在内存的一端,从而释放出大块的连续空闲内存。这个过程的 Stop-The-World时间较长,但ART会尽量在后台或应用在后台时进行。
 
- 分代收集:这是现代GC算法的核心思想,ART也采用了。
 
四、 深入理解:分代收集理论
这是理解现代GC(包括ART)的关键。根据对象的存活周期,将Java堆划分为新生代和老年代。
1. 新生代
- 
存放对象:绝大多数新创建的对象。 
- 
特点:“朝生夕死”,大部分对象很快变得不可达。 
- 
区域划分: - Eden区:新对象基本都分配在这里。
- Survivor区(两个):From Survivor和To Survivor。用于存放每次Minor GC后存活下来的对象。
 
- 
回收过程(Minor GC): - 新对象在Eden区分配。
- 当Eden区满时,触发一次 Minor GC。
- 将Eden区和From Survivor中仍然存活的对象,一次性复制到To Survivor区。
- 同时,为这些存活的对象“年龄”+1(每熬过一次Minor GC,年龄就加1)。
- 清空Eden区和刚使用的From Survivor区。
- 最后,From Survivor和To Survivor的角色互换。
 - 优点:速度非常快,因为只处理一小块区域。
- 晋升:当一个对象的年龄增长到一定程度(默认15),就会被移动到老年代。
 
2. 老年代
- 存放对象:经过多次Minor GC仍然存活的对象(即长时间存活的对象),以及大对象(可能直接进入老年代)。
- 特点:对象存活率高。
- 回收过程(Major GC / Full GC):
- 当老年代空间不足时,会触发 Major GC,通常会伴随一次 Minor GC,因此也常被称为 Full GC。
- Full GC 会对整个堆(新生代 + 老年代)进行回收,停顿时间最长,是应用卡顿的主要元凶之一。我们的优化目标就是尽量避免或减少Full GC。
 
五、 对Android开发的指导意义与实践
理解了GC原理,我们就能写出对GC更友好的代码,提升应用性能。
1. 内存泄漏是头号大敌
内存泄漏会导致对象永远无法被GC回收,最终引发OOM。
- 常见场景:
- 非静态内部类/匿名类持有外部类引用:如Handler、Runnable等。如果它们在Activity销毁后仍被系统(如消息队列)持有,就会导致Activity泄漏。
- 静态变量持有Context/View引用。
- 集合类未及时清理。
- 第三方库使用后未正确释放(如监听器、广播)。
 
- 排查工具:Android Profiler, LeakCanary。
2. 避免创建不必要的对象
对象的创建和销毁都是有成本的。在性能敏感的代码段(如onDraw、循环体),应尽量避免创建临时对象。
- 反面教材:// 在onDraw中每次循环都创建新对象,会瞬间产生大量垃圾,频繁触发GC for (int i = 0; i < 1000; i++) {String temp = "Item " + i; // 不要这样做!canvas.drawText(temp, x, y, paint); }
- 优化方案:将对象提升为成员变量,或使用对象池。
3. 谨慎使用 System.gc()
如前所述,显式调用GC会打乱虚拟机的优化策略,可能导致不必要的、耗时的Full GC。把内存管理的决策权交给虚拟机。
4. 关注 onTrimMemory()
当系统内存不足时,会回调此方法。我们可以在这里释放一些非核心资源(如缓存图片),帮助系统减轻内存压力,从而降低自身进程被杀死和触发GC的概率。
总结
| 特性 | Dalvik GC | ART GC | 
|---|---|---|
| 核心目标 | 功能实现 | 减少停顿,提升性能 | 
| 主要算法 | 标记-清除(为主) | 并发标记-清除 + 分代收集 + 压缩 | 
| 停顿时间 | 较长,易引起卡顿 | 显著缩短,更流畅 | 
| 处理碎片 | 不处理,碎片化严重 | 通过压缩整理内存 | 
作为Android开发者,理解GC原理的最终目的是:
- 写出高性能、低卡顿的代码:通过避免内存泄漏和减少不必要的对象分配。
- 快速定位和解决内存问题:当发生OOM或内存抖动时,能迅速找到根源。
- 建立良好的内存观:知道代码的每一行背后可能发生什么,做到心中有数。
希望这份详细的讲解能帮助你彻底理解Android GC!
