OutOfMemoryError — 一个致命的异常,当Java虚拟机(JVM)或Android运行时(ART)由于堆(Heap)空间不足而无法为新对象分配内存时发生。根据Square Engineering的数据,70%的移动应用中的OutOfMemoryError是由内存泄漏引起的,而不是实际超出限制。理解OOM的原因是应用稳定运行的关键。
要点
OutOfMemoryError(OOM)— 是Java/Kotlin中VirtualMachineError家族的异常,表示无法为新对象分配内存。与受检异常不同,OOM是错误(Error),不需要通过catch处理——尽管技术上可以捕获。OOM发生后,应用通常处于不稳定状态,建议关闭它。
在Android上,每个应用都有一个由设备制造商设置的Heap限制。对于配备6+ GB RAM的现代智能手机,限制为256–512 MB,对于预算设备——128–192 MB。当所有存活对象的总大小超过此限制时,ART会抛出OutOfMemoryError。
重要的是要理解:OOM并不总是意味着设备的物理内存已耗尽。这意味着应用已经用完了系统设置的Heap限制。其他应用可能有空闲内存,但由于Android中的进程隔离,您的应用无法使用它。
五种场景经常导致移动应用中的OOM。每种场景都与特定类型的数据或操作相关。
Bitmap — Android应用中的主要内存消耗者。以原始大小加载FullHD图像(1920 × 1080)在ARGB_8888格式下占用8.3 MB。如果RecyclerView中有50张这样的图像——那就是415 MB,超过任何设备的Heap。加载没有inSampleSize的图像——在弱设备上保证OOM。
使用Glide或Coil进行自动缩放。这些库以适合View的大小加载图像,而不是原始分辨率。对于直接使用BitmapFactory.Options,应用inSampleSize:将其计算为2的幂,以便最终大小不超过2048 × 2048像素。此外,对于没有透明度的图像,使用RGB_565代替ARGB_8888——这可以将内存消耗减半。
fun loadScaledBitmap(path: String, reqWidth: Int): Bitmap? {
val opts = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
BitmapFactory.decodeFile(path, opts)
opts.inSampleSize = calculateSampleSize(opts.outWidth, reqWidth)
opts.inJustDecodeBounds = false
return BitmapFactory.decodeFile(path, opts)
}
一个几KB的泄漏不会导致OOM。但每个屏幕上的数十个泄漏会累积:每次切换到屏幕都会增加一个泄漏,GC无法释放对象,堆被填满。典型模式:用户打开和关闭个人资料屏幕20次→Heap增长200 MB→应用因OOM崩溃。
在项目中安装LeakCanary以自动检测泄漏。它将显示每个泄漏的对象及其准确的堆栈跟踪。修复所有泄漏后,Heap消耗变得稳定:关闭屏幕后,内存恢复到基线水平。
将文件完整加载到byte[]中——通往OOM的直接途径。一个50 MB的JSON文件在解析时会创建一个相同大小的字符串加上DOM模型。加载到内存中的视频文件、音频缓冲区和大型protobuf数据集——所有这些都可能在一次操作中超过Heap限制。
使用流处理大数据:带4–8 KB缓冲区的InputStream,流式JSON解析器(Jackson或Gson配合JsonReader),用于视频的MediaCodec。永远不要在大于可用Heap 10%的文件上调用File.readBytes()。
在循环中大量创建对象而没有中间GC可能导致OOM,特别是在Heap较小的设备上。示例:在for循环中生成100,000个对象,这些对象在GC有机会收集它们之前无法放入Heap。这在游戏和图形编辑器中更常见。
对批量创建和销毁的对象使用对象池。对于数值数据,使用原始类型(FloatArray代替List<Float>)。使用ViewHolder Pool的RecyclerView解决了UI组件的这个问题。
碎片化——总空闲内存足够,但没有连续块用于新对象的状态。ART在GC期间压缩Heap,但并不总是成功。大数组(Bitmap、byte[])对碎片化最敏感。
Android 8+上的ART使用分代GC,通过分离年轻和老对象来减少碎片化。尽管如此,避免在同一个池中分配不同大小的碎片——尝试使用预先分配的固定大小缓冲区。
Heap限制在Android中不是常量——取决于制造商、设备型号和操作系统版本。Google通过兼容性定义文档(CDD)设定最低要求,但制造商设定实际值。
| 设备类别 | 典型Heap | largeHeap |
|---|---|---|
| 预算型(1–2 GB RAM) | 128–192 MB | 256–384 MB |
| 中端(3–4 GB RAM) | 256–384 MB | 512 MB |
| 旗舰型(6+ GB RAM) | 384–512 MB | 768 MB–1 GB |
| 平板(4+ GB RAM) | 256–512 MB | 768 MB |
| Wear OS | 32–64 MB | 无 |
可以通过清单中的android:largeHeap="true"请求提高限制。谨慎使用:增加Heap不能解决泄漏问题,如果系统被迫杀死其他应用以释放内存,可能会恶化用户体验。对于Wear OS,Heap限制最小——仅32–64 MB,这里largeHeap不可用,节约内存加倍重要。
诊断OOM需要分析Heap Dump并理解哪些对象消耗内存。Android Studio提供所有必要的工具。
步骤1:捕捉OOM时刻。在Android Memory Profiler中,点击Record memory allocations并执行导致崩溃的场景。分析器将显示OOM前的分配跳跃。如果OOM无法重现,通过android:smallHeap在debug构建中减少Heap,或使用DDMS手动调用GC。
步骤2:在峰值负载时(OOM之前)获取Heap Dump。在Android Studio中打开Dump:Classes选项卡按Retained Size排序。最大的对象——Bitmap、byte[]、String。对于每个Bitmap,检查大小(width × height × 4字节)和通过堆栈跟踪的加载路径。
步骤3:分析重复对象的数量。如果您看到200个相同的Fragment或Activity——那是泄漏。如果500个相同大小的Bitmap——图像缓存问题。MAT(内存分析器工具)通过支配树提供更深入的分析,显示哪些对象持有80%的Heap。
// 通过adb执行Heap Dump的命令
adb shell am dumpheap com.example.app /data/local/tmp/dump.hprof
adb pull /data/local/tmp/dump.hprof.
全面的OOM预防策略包括五个保护级别:从架构解决方案到生产中的监控。
ViewModel + Repository模式将数据与UI分离,防止屏幕旋转时持有View。ViewModel的寿命比Activity更长,其数据不会丢失,View可以重新创建而不会在内存中重复数据。使用StateFlow代替LiveData进行显式状态管理。
Glide — 处理图像的强制库。它自动缩放、缓存(磁盘+内存)和回收Bitmap。为大型列表配置diskCacheStrategy和skipMemoryCache。对于动画图像,使用带GIF/WebP的Glide——它们比Bitmap序列占用更少内存。
Firebase Performance Monitoring实时跟踪内存消耗。设置Heap使用超过限制80%的警报——这是检查的信号。Crashlytics将OOM作为异常收集,并显示崩溃前最后已知的Heap状态。对于Android 11+,使用ApplicationExitInfo检测OOM终止。
必须在具有最小Heap(128–192 MB)的设备上测试应用。带小屏幕和小Heap的模拟器模拟预算设备。如果应用在这样设备上运行,在旗舰设备上不会有OOM问题。使用Firebase Test Lab与不同价格类别的真实设备进行测试。
// 在重操作之前检查可用Heap
fun canAllocate(requiredBytes: Long): Boolean {
val runtime = Runtime.getRuntime()
val free = runtime.freeMemory()
return free > requiredBytes * 2 // 50%备用
}
常见问题
技术上可以,但不建议这样做。OOM后,应用处于不稳定状态:新的分配可能不起作用,某些对象可能部分创建。catch中唯一合理的操作——记录日志和重启Activity。
Heap限制在不同设备上不同。需要300 MB的操作在限制为192 MB的设备上会崩溃,但在512 MB的旗舰设备上会通过。在最低规格的设备上测试以发现OOM场景。
largeHeap增加限制,但不会加快应用速度。GC暂停时间变得更长,因为收集大型Heap需要更长时间。系统可能会终止后台应用以提供内存。仅对客观需要大量内存的应用(相机、编辑器)使用largeHeap。
OOM — 应用内部在Heap不足时的异常。系统杀死(Low Memory Killer)— Linux内核决定杀死进程以释放内存供其他应用使用。系统杀死时,应用不会收到异常——进程只是终止。
公式:width × height × bytesPerPixel。ARGB_8888 = 4字节/像素,RGB_565 = 2字节/像素。ARGB_8888格式的FullHD Bitmap(1920 × 1080)= 8.3 MB。4K Bitmap(3840 × 2160)= 33 MB。始终将图像缩放到在屏幕上显示所需的大小。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。