内存优化与泄漏排查
内存问题直接影响 App 的存活率(OOM)和流畅度(频繁 GC 导致卡顿)。对于底层背景开发者,native 内存泄漏排查会是很好的加分项。
一、常见的内存泄漏场景
内存泄漏(Memory Leak)的本质是:长生命周期的对象(如单例、静态变量、系统级线程)持有了短生命周期对象(如 Activity/Fragment)的强引用,导致短生命周期对象无法被 GC 回收。
Java / Kotlin 泄漏重灾区
- 静态变量 / 单例:静态变量持有 Activity 的 Context 或 View 实例。
- 非静态内部类 / 匿名类:如
Handler延时消息未移除、Runnable在子线程还在跑,它们隐式持有外部类的引用。 - 未注销监听器:广播接收器
BroadcastReceiver、EventBus 等未在onDestroy注销。 - 协程 / Flow:未跟随组件生命周期(如在 Activity
onDestroy后协程仍在运行),或全局ViewModel持有了 UI 元素。 - 资源未关闭:
Cursor、Stream、FileDescriptor没有close,或者Bitmap未及时recycle。
二、内存抖动与 GC 机制
- 内存抖动 (Memory Churn):短时间内频繁创建大量临时小对象(如在
onDraw或大循环内new对象),导致可用内存迅速耗尽,触发频繁 GC。 - 影响:Dalvik/ART 的 GC 过程可能会产生应用停顿(Stop-The-World,尽管 ART 有并发 GC,但仍有停顿和 CPU 开销),从而引发界面卡顿甚至掉帧。
- 优化:使用对象池复用对象,避免在高频调用的路径(如 UI 渲染)中进行堆分配。
三、Bitmap 与 Native 内存
现代 Android (8.0+) 的 Bitmap 像素数据存放于 Native 内存中,这大大缓解了 Java 堆 OOM 的压力,但也带来了 Native 泄漏的风险。
- Native OOM:当系统总可用物理内存不足或虚拟地址空间耗尽时,Native 层同样会发生分配失败(OOM)。
- 排查手段:Native 内存的追踪较难,可以通过
malloc_debug或HWASan/ASan等底层工具进行内存越界和泄漏排查。
四、排查路径与流程(本地 vs 线上)
诊断策略需区分开发期和生产环境:
| 诊断层级 | 工具与手段 | 适用场景 |
|---|---|---|
| 本地定位 | LeakCanary、Memory Profiler、MAT | 开发期主动发现泄漏、详细分析引用链 |
| 线上监控 | 内存阈值报警、KOOM(快手开源的线上 OOM 治理)、APM 平台 | 捕获真实用户场景 OOM、轻量级 dump 和裁剪上报 |
泄漏排查流程:
- 现象发现:观察到应用内存占用持续走高或收到 OOM 奔溃报告。
- 触发 Dump:在开发阶段,利用 Memory Profiler 点击强制 GC 并抓取 Java Heap (hprof)。LeakCanary 会在后台对象保留超时后自动 dump。
- 寻找 GC Roots:将 hprof 导入 Memory Profiler 或 MAT (Memory Analyzer Tool)。寻找怀疑泄漏的对象实例(如
MainActivity)。 - 分析引用链 (Reference Chain):查看从 GC Root 到该泄漏实例的最短强引用路径。
- 切断引用:修改代码,将该路径上的长生命周期引用置为 null、改用弱引用 (
WeakReference),或在合适的生命周期节点手动解绑。
五、LeakCanary 原理剖析
LeakCanary 是如何自动发现泄漏的?
- Hook 生命周期:自动注册
Application.ActivityLifecycleCallbacks(或利用 Fragment/ViewModel 钩子)监听对象销毁。 - 弱引用观察:对象销毁后,用它构建一个带有
ReferenceQueue的弱引用,交由ObjectWatcher观察,并标记个超时时间(如 5s)。 - 判断回收:超时后强制触发一次 GC,如果该弱引用没有出现在
ReferenceQueue中,说明对象还被强引用,标记为retained。 - 抓取分析:若
retained对象数达阈值,dump 整个 hprof 文件,交由内嵌的 Shark 库解析 hprof,找出最短强引用链并发送通知。
高频面试题
Q1: 内部类一定会导致内存泄漏吗? 不一定。只有非静态内部类才会隐式持有外部类的引用。且只有当内部类的生命周期长于外部类时(比如被 static 变量引用,或跑在一个没结束的子线程里),才会造成外部类无法释放,形成泄漏。静态内部类不持有外部类引用。
Q2: Handler 导致的内存泄漏怎么解决?
将 Handler 声明为静态内部类,内部通过 WeakReference<Activity> 持有 Activity 以便更新 UI。更关键的是,在 Activity onDestroy 中调用 handler.removeCallbacksAndMessages(null) 清除所有未执行的消息和任务。
Q3: Android 8.0 之后 Bitmap 内存存在哪里?如果 OOM 了怎么排查? 存放于 Native 内存中。Java 层面的 OOM (OutOfMemoryError) 和 Native 内存溢出不同。排查时可以抓取 hprof,看是不是应用中有大量的 Bitmap 引用没有被回收。线上可以用 KOOM 等库,利用 fork 子进程的方式 dump 内存,减少对主进程的影响。
易错点 / 追问
- 误区: “使用弱引用可以解决所有内存泄漏”。很多时候泄漏是因为生命周期管理错乱,比如网络请求没取消导致回调还在等。粗暴地改成弱引用可能掩盖逻辑漏洞(回调突然没反应了)。正确的做法是绑定生命周期及时取消任务。
- 追问: LeakCanary 会影响生产环境的性能吗?(答:会,抓取 hprof 时进程会发生长达数秒的冻结,且分析过程耗 CPU 和内存,所以通常只在 debug 环境或灰度阶段启用。)
- 易错点: 分析 MAT 时死盯所有引用链。只需关注强引用 (Strong Reference),软/弱/虚引用不会阻止对象被垃圾回收。