Android 中常见的 OOM(Out Of Memory)场景,可以从Java 堆内存、Native 内存、Bitmap 内存、线程泄漏、大对象分配等几个维度来梳理。
一、Java 堆内存 OOM
1. 内存泄漏导致堆持续增长
典型场景:
| 泄漏类型 | 具体表现 | 根因 |
|---|---|---|
| 单例持有 Activity | 退出 Activity 后对象无法回收 | static 单例引用 Activity 作为 Context |
| Handler / Runnable 匿名内部类 | 发送的延迟消息持有外部 Activity 引用 | 匿名内部类隐式持有外部类引用,消息未处理完 Activity 无法销毁 |
| 监听器未注销 | 广播、传感器、定位、网络回调持续持有 | registerReceiver / registerListener 后未 unregister |
| 资源未关闭 | Cursor、File、Stream、数据库连接泄漏 | 未在 finally 中关闭或用完未置 null |
| WebView | 退出页面后内存不降 | WebView 持有 Activity 引用,且 Native 层有独立渲染线程 |
修复方向:
- 单例用 ApplicationContext 替代 ActivityContext
- Handler 改为静态内部类 + WeakReference,Activity onDestroy 时 removeCallbacksAndMessages(null)
- 所有注册成对注销,用 LifecycleObserver 自动管理
- 资源用 try-with-resources 或 use 块
- WebView 单独进程 + onDestroy 中 destroy() + 从父容器 removeView
2. 集合类无限增长
典型场景:
- List、Map、Set 作为成员变量,只 add 不 remove
- 缓存无上限,如图片内存缓存、网络请求缓存
- 日志缓冲区、事件总线(EventBus)订阅者列表膨胀
修复方向:
- 使用 LruCache 替代无界 HashMap,设置合理 maxSize
- 用 WeakHashMap 或 SoftReference 做二级缓存
- 定期清理或设置 TTL(Time To Live)
3. 字符串 / 大对象频繁创建
典型场景:
- 循环内大量 String 拼接(产生大量中间对象)
- JSON 解析大文件时一次性加载到内存
- XML / HTML 解析用 DOM 方式(整棵树驻留内存)
修复方向:
- 循环内用 StringBuilder 替代 + 拼接
- 大文件用流式解析(JsonReader、XmlPullParser)
- 避免在 onDraw、onTouchEvent 中创建对象
二、Bitmap 内存 OOM
这是 Android 中最常见的 OOM 类型之一。
| 场景 | 说明 |
|---|---|
| 加载超大分辨率图片 | 直接 BitmapFactory.decodeFile() 加载 4K 图片,不采样 |
| 图片列表快速滑动 | RecyclerView 中每个 Item 都加载原图,无复用 |
| 内存中缓存过多大图 | 图片内存缓存无上限,同时持有几十张高清图 |
| ARGB_8888 格式滥用 | 每个像素 4 字节,一张 1920×1080 图约 8MB |
修复方向
java
// 1. 采样加载:inSampleSize
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
options.inJustDecodeBounds = false;
Bitmap bitmap = BitmapFactory.decodeResource(res, resId, options);
// 2. 复用 Bitmap 内存:inBitmap(API 11+)
options.inBitmap = reusableBitmap;
// 3. 用 Glide / Coil 自动管理
// 内部已封装采样、缓存、复用、生命周期绑定
Glide.with(context).load(url).into(imageView);
三、Native 内存 OOM
典型场景
| 场景 | 说明 |
|---|---|
| Bitmap 像素数据(API 10 前) | 旧版本 Bitmap 像素存在 Native 堆,Java 堆只持有一个 int 指针 |
| 大量 JNI 调用未释放引用 | NewGlobalRef / NewLocalRef 后未 DeleteGlobalRef |
| Native 库内存泄漏 | 第三方 so 库(如地图、音视频)内部泄漏 |
| RenderScript / OpenGL 纹理 | 纹理对象未释放,GPU/Native 内存持续增长 |
修复方向
- 及时调用 bitmap.recycle()(API 10 前必须)
- JNI 层严格配对 NewGlobalRef / DeleteGlobalRef
- 用 NativeMemoryProfiler 或 malloc_debug 追踪 Native 分配
- 谨慎使用 android:largeHeap="true",它只是推迟 OOM,不是解决方案
四、线程泄漏导致 OOM
典型场景
| 场景 | 说明 |
|---|---|
| AsyncTask 滥用 | 每个网络请求都 new AsyncTask,线程池无限增长 |
| 自定义 Thread 不停止 | 后台轮询线程 while(true) 无退出条件 |
| 线程池参数不当 | Executors.newCachedThreadPool() 无界队列,任务堆积 |
| Timer / ScheduledExecutor 未取消 | 定时任务持续创建新线程 |
修复方向
java
// 用协程或有限线程池
ExecutorService executor = Executors.newFixedThreadPool(4);
// Kotlin 协程
lifecycleScope.launch(Dispatchers.IO) {
// 自动绑定生命周期,退出时取消
}
// WorkManager 替代常驻后台线程
PeriodicWorkRequest work = new PeriodicWorkRequest.Builder(
MyWorker.class, 15, TimeUnit.MINUTES).build();
WorkManager.getInstance(context).enqueue(work);
五、大对象分配直接触发 OOM
典型场景
| 场景 | 说明 |
|---|---|
| 一次性加载超大数组 | byte[100 * 1024 * 1024] 直接申请 100MB |
| 大文件转 Base64 | 10MB 文件转 Base64 后字符串约 13MB,再复制一次约 26MB |
| 深拷贝大对象 | 序列化反序列化大对象图 |
| 反射创建大对象 | Array.newInstance() 传入超大 size |
修复方向
- 大文件分片处理,用流式读写
- 避免不必要的对象拷贝,用 ByteBuffer 直接操作
- 反射创建数组前校验 size 合理性
六、系统限制导致的 OOM
| 场景 | 说明 |
|---|---|
| App 堆内存上限 | 低端机 16MB,中端 64MB,高端 256MB+,由 dalvik.vm.heapsize 决定 |
| 进程内存上限 | 32 位进程虚拟地址空间 3GB,大量 so 映射 + 堆分配可能耗尽 |
| FD(文件描述符)耗尽 | 超过 1024 个 FD 后 open() 失败,间接导致内存分配异常 |
七、OOM 排查工具链
| 工具 | 用途 |
|---|---|
| Android Studio Memory Profiler | 实时查看堆内存,触发 GC,Dump HPROF |
| LeakCanary | 开发期自动检测 Activity / Fragment 泄漏,生成引用链 |
| MAT (Memory Analyzer Tool) | 分析 HPROF 文件,查找 Dominator Tree、Path to GC Roots |
| StrictMode | 检测 Activity 泄漏、未关闭资源、SQLite 未关闭 |
| adb shell dumpsys meminfo | 查看进程 PSS / USS / VSS 内存分布 |
| Native Memory Profiler | Android Studio 4.1+ 支持 Native 堆分析 |
八、总结:OOM 预防 checklist
□ 单例用 ApplicationContext,不用 Activity Context
□ Handler 改为静态内部类 + WeakReference,onDestroy 移除消息
□ 所有 register 成对 unregister,用 LifecycleObserver 自动管理
□ 资源 try-with-resources 关闭,WebView 单独进程并 destroy
□ Bitmap 用 inSampleSize 采样 + inBitmap 复用,或用 Glide/Coil
□ 缓存用 LruCache,设置合理上限和过期策略
□ 线程用协程 / FixedThreadPool / WorkManager,避免无限创建
□ 大文件流式处理,避免一次性加载到内存
□ 定期 Memory Profiler 检查堆增长趋势,线上监控 OOM 率
OOM 的本质是内存分配请求超过了可用内存,要么减少分配,要么及时释放,要么提升上限(但提升上限是下策)。