一、内存管理基础
做 Android 开发这么多年,我见过太多因为内存问题导致 App 崩溃的案例。说白了,内存管理就是个基本功。你连 Java 堆和 Native 堆都分不清,遇到 OOM 就只能干瞪眼。这一节咱们从最底层说起。
核心观点:Android 内存管理 = Java 堆 + Native 堆 + GC 回收机制。搞懂这三样,你就能看懂内存快照,精准定位问题。
1.1 Android 内存模型概览
每个 Android 应用都运行在独立的 Dalvik/ART 虚拟机中。系统会为每个进程分配一个固定的 heap 上限 — 这个值因设备而异,低端机可能只有 64MB,高端机可以到 512MB 以上。我早期在一款 2GB RAM 的平板上调试过,heap 上限只有 128MB,稍微加载几张高清图就崩了。
来看下整体结构:
1.2 Java 堆与 Native 堆:本质区别
很多刚入行的同学以为 Android 只有 Java 堆,其实 Native 堆才是内存大户。你想想看,图片、音视频、WebView 渲染数据都跑在 Native 堆上。我曾在优化一个视频播放器时发现,Java 堆只用了 30MB,但 Native 堆飙到了 400MB — 直接导致系统杀进程。
下面这张表我经常贴在工位上,面试也爱问:
| 对比维度 | Java 堆 | Native 堆 |
|---|---|---|
| 分配方式 | 由 GC 自动管理 | malloc / new 手动分配 |
| 回收方式 | GC 自动触发 | 必须手动 free / delete |
| 内存上限 | 受制于 maxMemory() |
受制于物理 RAM + Swap |
| 常见溢出类型 | Java OOM (OutOfMemoryError) | Native OOM (往往直接崩溃) |
| 调试工具 | Android Profiler, MAT, LeakCanary | AddressSanitizer, malloc debug |
这里有个坑:很多人用 dump hprof 分析 Java 堆觉得没问题,但 App 依然闪退 — 十有八九是 Native 堆爆了。我个人习惯在监控平台同时打两个桩:Java heap 和 Native heap 曲线。
小技巧:可以用 adb shell dumpsys meminfo 快速查看 Java Heap + Native Heap 的实时占用。
1.3 GC 原理与触发时机
ART 虚拟机(Android 5.0 之后)使用分代垃圾回收,核心思路就是:「大多数对象朝生夕死」。基于这个假设,ART 把 Java 堆划分成三个空间:
- 年轻代 (Young Generation):新对象在这里分配,GC 频率高但速度快。
- 老年代 (Old Generation):经过多次 GC 还存活的对象,移到这里,回收频率低。
- 大对象堆 (Large Object Space):专门存放 byte[] 等大对象,比如 Bitmap 的像素数据。
为什么分代能提速?我举个例子:你做一个列表页,每次上下滑都会创建几十个 ViewHolder,用完就扔。大部分对象在年轻代就被回收了,不会跑到老年代去折腾。如果你不小心把 ViewHolder 引用静态住了,它们就会晋升到老年代 — 这就是内存泄漏的雏形。
GC 触发时机主要有以下几种(我按出现频率排):
- 阈值触发:分配新对象时,发现年轻代剩余空间不足,触发 Minor GC。
- 显式调用:代码里写了
System.gc()或Runtime.getRuntime().gc()— 但这只是建议,ART 可能忽略。 - 对象晋升:年轻代 GC 后仍有大量存活对象,被迫晋升到老年代,可能触发 Full GC。
- 内存压力:系统整体内存不足,发送
onTrimMemory()级别回调,ART 会主动执行 GC。
嗯,这里要注意:Full GC 会暂停所有应用线程。如果你的 App 在滚动过程中突然卡顿一下,很可能就是发生了 Full GC。我曾在直播项目中遇到过 — 用户的弹幕刷得正欢,突然冻住 200ms,体验巨差。后来排查发现是我们把弹幕对象都缓存到了一个静态 ArrayList,导致大量对象进入老年代,触发 Full GC。
// 查看当前 GC 状态的 adb 命令
adb shell dumpsys activity processes | grep "heap"
// 输出示例:
// heap: 50% free, 64MB allocated, 128MB max
// gc: 3 concurrent mark sweep collections, 0.12s total
避坑指南:我曾经把 Bitmap 全部放在 LruCache 里,以为控制了总量就没问题。结果 LruCache 内部持有的是 Activity 的引用,Bitmap 被移出后仍被 Fragment 持有,导致几十个 Bitmap 滞留老年代。每次翻页都触发 Full GC。解决方式:务必使用弱引用或主动 recycle(虽然官方不推荐,但低端机还是需要)。
1.4 内存泄漏的早期诊断手段
光知道理论不够,你得能发现它。我常用的三板斧:
- LeakCanary:自动检测 Activity / Fragment 泄漏,直接发通知。适合开发阶段。
- Android Profiler:观察 heap 曲线,如果只升不降,基本就是泄漏。
- 日志关键字:抓取
am_onLowMemory、AppDeathReason、AVIEW等系统日志。
这里说个真实案例:有一次线上反馈说 App 切后台再切回来就白屏。我打开 Profiler 一看,每次切后台 Heap 都飙高 20MB,但切回来又不降。逐层排查发现是 WebView 的 CookieManager 单例持有了 Activity 引用。这种泄漏不严重但积少成多,最终触发系统杀进程。解决办法:在 onDestroy() 里显式清空 WebView 相关回调。
小结:Java 堆泄漏通过 LeakCanary 能快速定位;Native 堆泄漏比较隐蔽,建议借助 AddressSanitizer 或者手机厂商的内核日志。下一层我们专门讲如何用 MAT 分析堆快照。