通过垃圾回收实现自动内存管理是 Android 平台的关键机制,基于 ART 虚拟机。根据 Google Android Documentation, 2026,垃圾回收器将开发人员从手动内存管理中解放出来,自动删除不再有引用的对象。如果没有 GC,每次对象分配都需要显式调用 free 或 delete,这在每秒处理数百万对象的 Java 生态系统中是物理上不可能的。
要点
垃圾回收 (GC) — 是自动检测和释放程序中不再使用的对象所占内存的过程。在移动开发的背景下,GC 通过 ART 虚拟机以及标准 Java 虚拟机应用于 Android 平台。
与手动内存管理的语言(C、C++)不同,程序员必须显式调用 free 或 delete,GC 完全接管了追踪对象生命周期的工作。开发人员通过 new 运算符创建新对象,回收器确定对象变得不可达的时刻 — 即没有剩余任何活动引用指向该对象。
GC 效率的主要指标 — 暂停时间和吞吐量。暂停是为执行回收而暂停应用程序执行的时间段。在移动环境中,超过 8–16 毫秒的暂停会被察觉为丢帧 (jank)。
根据 Google I/O 2019 数据,Android 10 中的 ART 将典型的 GC 暂停时间减少到 2–4 毫秒,比 Android 4.4 中的 Dalvik 减少了 70%。尽管如此,不正确的内存操作 — 在循环中频繁分配对象、不必要地创建临时实例 — 仍然是性能问题的主要原因。
Java 和 Android 中的所有 GC 实现都基于几个基本算法,这些算法组合起来在暂停时间和清理完整性之间取得平衡。理解这些算法对于编写 GC 友好型代码 是必要的。
Mark-and-Sweep — 最简单的算法,分两个阶段工作。在 Mark 阶段,回收器遍历对象图,从根引用(root set)开始 — 局部变量、静态字段、线程栈。每个可达对象都用 live 标志标记。在 Sweep 阶段,回收器遍历整个堆并释放未标记对象的内存。
缺点 — 内存碎片化:Sweep 后,空闲区域与占用区域交替出现,这使得大对象的分配变得困难。在移动场景中,这很关键,因为堆通常很小(Android 上为 64–512 MB)。
Copying Collection 将堆分成两个半空间(semi-spaces)。活动对象从一个半空间复制到另一个半空间,紧凑无间断。复制后,旧半空间被完全声明为空。该算法完全消除了碎片化,但需要两倍的内存。
在移动环境中,Copying Collection 被分代回收器用于快速清理年轻对象,这些对象在统计上会提前死亡(弱分代假设)。
Generational Collection 将堆分成代:Young Generation(年轻对象)和 Old Generation(经过多次回收存活下来的老对象)。年轻代的回收(Minor GC)频繁且快速,因为大多数对象都是年轻时就死亡了。老年代的回收(Major GC 或 Full GC)发生频率较低,但持续时间更长。
// 分代 GC 演示:年轻对象快速死亡
void processItems(List<Item> items) {
List<Result> results = new ArrayList<>(); // 存活整个方法
for (Item item : items) {
Result r = new Result(item.getValue()); // 立即死亡
if (r.isValid()) {
process(r); // r 变成垃圾
}
}
saveResults(results); // results 迁移到 Old Gen
}
在此示例中,Result 对象在循环内创建并立即成为垃圾 — 它们是 Young GC 的理想候选对象。results 对象存活更长时间并迁移到 Old Generation。分代分离允许 Minor GC 在毫秒内清理年轻对象,而不触及老堆。
Android 经历了从 Dalvik VM 到 ART(Android Runtime)的发展历程,GC 的实现是两者之间的关键区别之一。理解 Android 中的 GC 架构有助于编写在真实设备上最小化暂停的代码。
| 特性 | Dalvik(至 4.4) | ART(5.0+) |
|---|---|---|
| GC 类型 | 带 Concurrent Mark 的 Mark-and-Sweep | 分代 + Concurrent |
| 典型暂停 | 10–30 ms | 2–4 ms |
| 压缩 | 无(碎片化不断增加) | 有(后台进行,不停止应用程序) |
| AOT 编译 | JIT(即时编译) | AOT + JIT(混合) |
Dalvik 使用 Mark-and-Sweep 与 concurrent 阶段相结合。Concurrent Mark 允许应用程序在遍历对象图期间继续工作,但 Sweep 阶段需要停止所有线程(Stop-The-World)。在内存较小的设备上(512 MB – 1 GB),暂停时间达到 30 毫秒,这导致了界面明显的卡顿。此外,Dalvik 不会压缩堆,因此长时间运行后碎片化增加,大对象(例如 Bitmap)的分配即使在总空闲内存充足的情况下也可能抛出 OutOfMemoryError。
ART(Android Runtime) 引入了带有并发压缩的分代回收器。堆被分成三个区域:Young、Mature(类似于 Old Generation)和 Large Object Space(用于大于 12 KB 的对象)。Young 区域的回收在大多数情况下可以并行进行而无需停止线程。在 Android 10+ 中出现了 Concurrent Copying — 压缩在后台线程中进行,无需 Stop-The-World。
得益于 ART 架构,典型的 GC 暂停时间减少到 2–4 毫秒,在年轻对象占优的场景中减少到 0.5–1 毫秒。这使得 Android 设备即使在活跃使用内存时也能提供稳定的 60 FPS。
在 Java 生态系统中存在多种 GC 实现,每种都有其自身的性能特征。对于 Android 开发,选择仅限于 ART,但了解 Java GC 在编写移动应用的服务器端以及使用 Kotlin Multiplatform 进行开发时很有用。
Serial GC — 单线程回收器,完全停止应用程序(Stop-The-World)。每个 Mark、Sweep 和 Compact 操作都由一个线程执行。性能低 — 不适用于移动服务器。仅适用于堆达 100 MB 的小型应用程序。
Parallel GC(也称为 Throughput Collector)使用多个线程进行所有回收阶段。它面向最大吞吐量 — 最小化在 GC 上花费的时间相对于应用程序运行的时间。通过 JVM 中的 -XX:+UseParallelGC 标志激活。
G1(Garbage-First)GC — Java 9+ 中的默认回收器。堆被分成 1–32 MB 的区域。G1 预测暂停时间并努力保持在设定的限制内(默认为 200 毫秒)。优先级:首先清理垃圾量最多的区域(因此得名)。G1 对于大堆服务器(4–64 GB)和可预测的暂停是高效的。
// 启用目标暂停时间为 100 毫秒的 G1 GC
// java -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -jar app.jar
public class MemoryMonitor {
private static final long THRESHOLD = 512 * 1024 * 1024; // 512 MB
public void checkHeapUsage() {
Runtime rt = Runtime.getRuntime();
long used = rt.totalMemory() - rt.freeMemory();
if (used > THRESHOLD) {
System.out.println("堆使用阈值已超:" + used);
System.out.println("考虑减少分配");
}
}
}
通过 Runtime 监控堆可以早期检测内存泄漏。如果在稳定运行中 used 超过最大堆的 80% — 这是可能发生泄漏或应用程序过度消耗内存的信号。
即使是最新式的 ART GC 也不能解决所有问题 — 不正确的内存使用仍然是 jank 和 ANR(应用程序无响应)的主要原因。让我们来看看主要场景和优化方法。
GC 暂停 — 回收期间应用程序线程的停止。在屏幕上,这表现为丢帧,当两帧之间的时间超过 16.6 毫秒(60 FPS)时。如果 GC 持续 30 毫秒,则只绘制一帧而不是两帧 — 用户会看到界面卡顿。
长时间暂停的主要原因:Old Generation 中大量活动对象、堆碎片化、频繁的 Full GC。诊断使用 Android Studio Profiler 和 systrace。
主要规则 GC 友好型代码 — 最小化分配的对象数量。每个新对象不仅需要分配内存,还需要后续回收。即使 GC 很快,每秒 1000 次额外分配也会给回收器带来 1000 次检查。
内存泄漏 发生在对象保持可达状态,但不再需要时。GC 无法删除此类对象,内存逐渐耗尽。典型原因:未注销的监听器、对 Activity 的静态引用、捕获外部上下文的匿名类以及未关闭的 Cursor/InputStream。
// 内存泄漏:匿名类持有对 Activity 的引用
public void startTask() {
new Thread(new Runnable() { // 隐式持有 this(Activity)
@Override
public void run() {
// 长时间操作...
System.out.println("完成");
}
}).start();
}
// 修复方案:静态嵌套类 + WeakReference
private static class TaskRunnable implements Runnable {
private WeakReference<Activity> activityRef;
TaskRunnable(Activity activity) {
this.activityRef = new WeakReference<>(activity);
}
@Override
public void run() {
Activity act = activityRef.get();
if (act != null) {
// 与 Activity 安全地工作
}
}
}
在此示例中,匿名 Runnable 捕获了对 Activity 的隐式引用。只要线程存活 — Activity 就不能被 GC 回收,即使用户已经关闭了屏幕。WeakReference + static class 修复方案打破了这个链条,使得 Activity 可以被回收。
常见问题
Android 中的 GC(ART)是一种带有并发压缩的分代回收器,针对内存受限的移动设备进行了优化。Java GC(G1、ZGC)是具有大堆和可预测暂停的服务器回收器。ART GC 不使用 JVM 标志 — 所有配置都在操作系统级别自动完成。
Stop-The-World — 回收器暂停所有应用程序线程以安全地遍历对象图或释放内存的时刻。STW 时间越长,jank 越明显。ART 凭借分代架构将典型的 STW 时间缩短到 2–4 毫秒。
使用 Android Studio Memory Profiler — 它显示堆增长、分配数量并允许进行 Heap Dump。进行深入分析时,使用 LeakCanary — 该库自动检测泄漏并显示阻止 GC 回收的引用链。
Full GC — 所有堆代的完全回收,包括 Old Generation。在移动应用中,Full GC 可能持续 50–200 毫秒,导致明显的 jank 或 ANR。主要原因:堆碎片化、内存泄漏、超过 Old Generation 阈值。
Kotlin 提供具有结构化并发性的协程 — 取消作用域会自动取消所有子协程,防止泄漏。此外,Kotlin 中还有用于延迟初始化的 lazy 委托以及减少临时对象数量的作用域运算符。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。