什么是垃圾回收(GC)?
垃圾回收,简称 GC(Garbage Collection),说白了就是 JVM / ART 自动帮我们回收不再使用的对象。
你想想看,C/C++ 里申请内存要自己 malloc/free,一不小心就内存泄漏。Java 和 Android 开发就舒服多了——对象 new 出来,用完了不用你管,GC 会在后台悄悄把没用的对象清理掉。
但问题也出在这里:GC 不是免费的。它需要暂停你正在跑的业务线程。暂停多了,帧率就崩了。
核心一句话:GC 就是自动化的内存回收机制,它负责把不会再被引用的对象标记出来,然后释放其占用的堆内存。
GC 的基本工作流程
不管哪种 GC 实现,大体流程都是类似的。我画了一张图帮你理清整个脉络:
GC 的核心环节拆解
1. 什么时候触发 GC?
不是随机的。常见的触发条件就这几个:
- 堆内存分配失败(Eden 区满了)
- 显式调用
System.gc()(不建议你这么干,除了某些场景) - JVM / ART 策略决定(比如老年代占用达到阈值)
避坑提醒:我曾经在一个 IM 项目里看到,开发为了“及时释放内存”每收到一条消息就调一次 System.gc(),结果手机发烫、帧率直接掉到个位数。千万别手贱主动调 GC。
2. 标记阶段:确定谁是垃圾
GC 从 GC Roots 出发,沿着引用链遍历,能到达的对象就是“活的”,不能到达的就是垃圾。
GC Roots 包括:栈帧中的局部变量、静态变量、JNI 引用等。
3. 回收阶段:把垃圾清走
标记完了,怎么清?不同算法不同策略:
| 算法 | 核心思路 | 优点 | 缺点 |
|---|---|---|---|
| 标记-清除 | 标记存活对象,清除所有未标记 | 实现简单 | 内存碎片,容易触发连续 OOM |
| 标记-复制 | 把存活对象复制到另一半空间 | 无碎片,分配高效 | 浪费一半内存,存活率高时性能差 |
| 标记-整理 | 标记存活,然后向一端移动,整理出连续空间 | 无碎片,内存利用率高 | 移动对象耗时更长 |
| 分代收集 | 新生代复制,老年代标记-整理 | 兼顾效率和碎片控制 | 实现复杂,调优参数多 |
个人经验:Android ART 默认使用的是并发标记 + 部分 Compaction 的算法。但即使在 ART 上,如果短时间内大量创建临时对象,依然会导致频繁 GC 和明显的掉帧。我习惯在性能分析时先看 GC 日志——
adb logcat | grep "GC" 能直接看到每次 GC 暂停了多少毫秒。
为什么 GC 会导致帧率下降?
原因很简单:GC 会“Stop-The-World”。
哪怕最先进的并发 GC,在标记初始、重新标记阶段也需要暂停所有应用线程。这个暂停时间虽然可能只有几毫秒,但如果 GC 频率高(比如每秒触发好几次),或者单次暂停超过 16.6ms,UI 线程就来不及绘制下一帧。
你想想看,用户正在滑动列表,突然 GC 暂停了 UI 线程,这一帧就丢了,肉眼就能感觉到卡顿。
Android 上 GC 的典型场景
- 频繁创建临时对象:比如在
onDraw里 new 一个 Paint、Path 或者 Bitmap,每帧都分配,很快撑爆新生代。 - 大量 String 拼接:用
+连接字符串,底层生成大量临时 StringBuilder 和 String 对象。 - 集合类频繁增删:比如 HashMap 不断 put 又 remove,会导致老年代膨胀。
- Bitmap 未及时回收:尤其是大图,占用巨大堆内存,很容易触发 Concurrent GC。
真实案例:我曾经优化过一个短视频 App 的 feed 流,每次滑到新卡片都会创建新的 ImageView 和 Bitmap,结果每 3-4秒触发一次 GC_FOR_ALLOC,暂停时间平均 12ms,帧率只能跑在 35fps 左右。后来改用 RecyclerView + 对象池,GC 频率降到了 2 分钟一次,帧率稳定在 58fps。
小结
GC 不是 bug,是 JVM/ART 的内存管理标配。但如果你不了解它的触发条件和暂停影响,写代码时稍微不注意,GC 就会变成帧率的杀手。
记住:GC 频率 × 单次暂停时间 = 掉帧总时长。下一阶段我们会深入分析如何通过日志和工具定位 GC 问题。