Android OOM微锦囊

2,812 阅读5分钟

一、OOM类型

1.1 java堆内存超限 + 无足够连续内存空间

传统的 java 堆内存超限,即申请堆内存大小超过了 Runtime.getRuntime().maxMemory();

此时打印的LOG信息长这样:

void Heap::ThrowOutOfMemoryError(Thread* self, size_t byte_count, AllocatorType allocator_type)
抛出时的错误信息:
oss << "Failed to allocate a " << byte_count << " byte allocation with " << total_bytes_free << " free bytes and " << PrettySize(GetFreeMemoryUntilOOME()) << " until OOM";

对于由于进程中存在大量的内存碎片,没有足够空间可以分配内存的,会额外多一个LOG信息:

failed due to fragmentation (required continguous
free “<< required_bytes << " bytes for anew buffer where largest
contiguous free "
<< largest_continuous_free_pages << " bytes)”;
其详细代码在 art/runtime/gc/allocator/rosalloc.cc

案例

这个案例介绍的是由于Handler使用不当(多次post,却没有及时remove)造成的OOM

由于sCompatVectorFromResourcesEnabled(会产生多个Resources实例,资源复用失败)不恰当使用,导致的OOM血案

1.2 线程数超限 + 虚拟内存不足

线程数超限,即proc/pid/status中记录的线程数(threads 项)突破 /proc/sys/kernel/threads-max 中规定的最大线程数。

可能的发生场景有:

  • app 内多线程使用不合理,如多个不共享线程池的 OKhttpclient 等等 ;

打印LOG信息:

void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon)
抛出时的错误信息:
"Could not allocate JNI Env"
或者
StringPrintf("pthread_create (%s stack) failed: %s", PrettySize(stack_size).c_str(), strerror(pthread_create_result)));

创建线程可以分为两个步骤:

  1. 调用 mmap 分配栈内存。这里 mmap flag 中指定了 MAP_ANONYMOUS,即匿名内存映射。这是在 Linux 中分配大块内存的常用方式。其分配的是虚拟内存,对应页的物理内存并不会立即分配,而是在用到的时候触发内核的缺页中断,然后中断处理函数再分配物理内存。
  2. 调用 clone 方法进行线程创建。

第一种是由于进程的虚拟内存不足,抛出错误如下:

W/libc: pthread_create failed: couldn't allocate 1073152-bytes mapped space: Out of memory
W/tch.crowdsourc: Throwing OutOfMemoryError with VmSize 4191668 kB "pthread_create (1040KB stack) failed: Try again"
java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Try again
at java.lang.Thread.nativeCreate(Native Method)
at java.lang.Thread.start(Thread.java:753)

第二种是线程数超出限制,clone方法失败

W/libc: pthread_create failed: clone failed: Out of memory
W/art: Throwing OutOfMemoryError "pthread_create (1040KB stack) failed: Out of memory"
java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Out of memory
at java.lang.Thread.nativeCreate(Native Method)
at java.lang.Thread.start(Thread.java:1078)

案例

这个案例是关于在部分华为手机上,当线程数超过500个,产生OOM的分析验证过程。

1.3 文件描述符 (fd) 数目超限

文件描述符 (fd) 数目超限,即 proc/pid/fd 下文件数目突破 /proc/pid/limits 中的限制。可能的发生场景有:短时间内大量请求导致 socket 的 fd 数激增,大量(重复)打开文件等 ;

二、如何定位OOM

首先要知道复现OOM的操作步骤,如果是随机测试出的,也需要找到一个有效的复现步骤才行。取操作前的 .hprof,和操作后,内存增长后的 .hprof。如果内存不断增长,可取3,4次。然后分别打开MAT的 直方图(Histogram)视图,在对象列表中,对比每个对象的 Retained size的变化。

  • ShallowSize:对象自身占用的内存大小,不包括它引用的对象。
  • RetainedSize:对象自身的 ShallowSize 和对象所支配的(可直接或间接引用到的)对象的 ShallowSize 总和,就是该对象 GC 之后能回收的内存总和。例如上图中,D 的` RetainedSize` 就是 D、H、I 三者的 ShallowSize 之和。

考虑到 RetainSize 越大的对象对内存的影响也越大,即 RetainSize 比较大的那部分 Instance 是最有可能造成 OOM 的“元凶”。

注:大对象 != 内存泄漏对象。排在第一位的对象,有可能它本身正常情况就很消耗内存,真正内存泄露的对象,是那个突然排名上升的。

三、OOM 监控措施

3.1 针对线程数/fd超限

可以利用 linux 的 inotify 机制进行监控:

  • watch /proc/pid/fd来监控 app 打开文件的情况,
  • watch /proc/pid/task来监控线程使用情况.

3.2 针对堆内存超限

单独开一个线程进行兜底,如图:

3.3 针对全局:Probe

Probe是美团出品的OOM检测工具,它能够有效定位线上 Java 堆内存不足、FD 泄漏以及线程溢出的 OOM 问题。感兴趣的可以了解下~

四、 如何避免OOM(内存优化经验)

  • 使用更加轻量的数据结构

使用 ArrayMap/ SparseArray替代HashMap等传统数据结构。 ArrayMap是Android系统专为移动操作系统编写的容器,在大多数情况下,比HashMap效率更高,占用内存更少。 SparseArray更加高效在于它们避免了对key和value的autobox自动装箱,并且避免了装箱后的解箱。

  • 避免在Android里面使用Enum
  • 减小Bitmap对象的内存占用
  • 使用更小的图
  • 避免在onDraw方法里面执行对象的创建
  • 注意缓存容器中的对象泄漏

如果容器是静态或者全局的,那么对于里面存放的对象要及时remove。

  • 检查内存泄漏,包括常见的Context泄漏、单例泄漏、EditText的TextWatcher泄漏等等,找到并fix他们,最简单的例子,能传application的地方就不要硬传个activity过去
  • 在Activity onDestory的时候,遍历View树,清空backGround、Drawable、EditText的TextWatcher等
  • Fresco的优化:RN中使用Fresco加载图片,在RN Activity销毁的时候,会将Fresco默认的memory cache清空,但是动图的缓存没有清

参考文章