浅谈Android OOM及定位分析

Posted 木大白易

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅谈Android OOM及定位分析相关的知识,希望对你有一定的参考价值。

OOM的原因

要定位OOM问题,首先需要弄明白android中有哪些原因会导致OOM,Android中导致OOM的原因主要可以划分为以下几个类型:

Android 虚拟机最终抛出OutOfMemoryError的代码位于/art/runtime/thread.cc。

void Thread::ThrowOutOfMemoryError(const char* msg)

参数 msg 携带了 OOM 时的错误信息

下面两个地方都会调用上面方法抛出OutOfMemoryError错误,这也是Android中发生OOM的主要原因。

堆内存分配失败

系统源码文件:/art/runtime/gc/heap.cc

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";

这是在进行堆内存分配时抛出的OOM错误,这里也可以细分成两种不同的类型:

  1. 为对象分配内存时达到进程的内存上限。由Runtime.getRuntime.MaxMemory()可以得到Android中每个进程被系统分配的内存上限,当进程占用内存达到这个上限时就会发生OOM,这也是Android中最常见的OOM类型。

  2. 没有足够大小的连续地址空间。这种情况一般是进程中存在大量的内存碎片导致的,其堆栈信息会比第一种OOM堆栈多出一段信息:failed due to fragmentation (required continguous free “<< required_bytes << “ bytes for a new buffer where largest contiguous free ” << largest_continuous_free_pages << “ bytes)”; 其详细代码在art/runtime/gc/allocator/rosalloc.cc中,这里不作详述。

创建线程失败

系统源码文件:/art/runtime/thread.cc

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)));

这是创建线程时抛出的OOM错误,且有多种错误信息。源码这里不展开详述了,下面是根据源码整理的Android中创建线程的步骤,其中两个关键节点是创建JNIEnv结构体和创建线程,而这两步均有可能抛出OOM。

创建JNI失败

创建JNIEnv可以归为两个步骤:

  • 通过Andorid的匿名共享内存(Anonymous Shared Memory)分配 4KB(一个page)内核态内存。
  • 再通过Linux的mmap调用映射到用户态虚拟内存地址空间。

第一步创建匿名共享内存时,需要打开/dev/ashmem文件,所以需要一个FD(文件描述符)。此时,如果创建的FD数已经达到上限,则会导致创建JNIEnv失败,抛出错误信息如下:

E/art: ashmem_create_region failed for 'indirect ref table': Too many open files
 java.lang.OutOfMemoryError: Could not allocate JNI Env
   at java.lang.Thread.nativeCreate(Native Method)
   at java.lang.Thread.start(Thread.java:730)

第二步调用mmap时,如果进程虚拟内存地址空间耗尽,也会导致创建JNIEnv失败,抛出错误信息如下:

E/art: Failed anonymous mmap(0x0, 8192, 0x3, 0x2, 116, 0): Operation not permitted. See process maps in the log.
java.lang.OutOfMemoryError: Could not allocate JNI Env
  at java.lang.Thread.nativeCreate(Native Method)
  at java.lang.Thread.start(Thread.java:1063)

创建线程失败

创建线程也可以归纳为两个步骤:

  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)

上述来自:Probe:Android线上OOM问题定位组件

下边重点了解一下虚拟内存!

虚拟内存

简单点来说,就是我们写程序时使用的地址称为虚地址或逻辑地址,其对应的存储空间称为虚拟地址空间或逻辑地址空间;而计算机物理内存的访问地址则称为实地址或物理地址,其对应的存储空间称为物理存储空间或主存空间。它是物理内存和进程之间的中间层,给进程提供了访问物理内存的抽象接口。我们不能直接操作物理内存地址,只有操作系统自己可以!

具体一点,对于Linux系统来说:

cpu寻址的最小单位是字节byte,32位能表示0~2^32个byte,换算一下就是4G。

为什么需要虚拟内存?

如果我们直接访问操作物理内存会有什么问题?如果一个计算机同时只运行一个程序,且它需要的内存不超过计算机的物理内存,这样是没有问题的,但是如果要同时运行多个程序,就会出现问题:

  • 地址空间不隔离:所有程序都直接访问物理内存,恶意程序可以修改别的程序的内存数据。
  • 内存使用效率低:因为程序运行时,就将整个程序装入内存中了,它需要的空间是连续的,如果A程序已经达到了最大内存空间,这时需要执行B程序,就需要将A换出。
  • 程序运行地址不确定:当程序装入内存中,就需要给它分配一块足够大的空闲空间,但是这块空间是不固定的,然而我们编写程序访问数据、执行指令大部分都是固定的。

CPU寻址:虚拟寻址

那么虚拟地址如何映射到真实的物理地址呢?

  • 分段
  • 分页
  • 分段+分页


简单来说,虚拟地址和物理地址的映射关系存储在页表(页表条目(Page Table Entry, PTE)的集合)中,cpu拿着虚拟地址,找到MMU,先去TLB(翻译旁路缓存器)中有没有缓存,有就直接拿到PTE了,然后就能翻译成对应的物理地址;如果TLB没有命中,MMU必须从内存中一级一级取出相应的PTE,再存到TLB中。

关于TLB可以参考:https://zhuanlan.zhihu.com/p/108425561

一些排查进程信息的命令

1.查看进程id

ps -ef|grep 'package name'

2.查看手机对进程的限制

cat /proc/pid/limits   

3.查看进程的所有线程

ps -T -p pid

VSZ 表示进程分配的虚拟内存(Virtual Memory Size)。包括进程可以访问的所有内存,包括进入交换分区的内存,被分配但是还没有使用,以及共享库占用的内存。
RSS 是常驻内存集(Resident Set Size),表示该进程分配的内存大小。包括共享库占用的内存(只要共享库在内存中)和所有分配的栈内存和堆内存。不包括进入交换分区的内存。
可以参考:https://stackoverflow.com/questions/7880784/what-is-rss-and-vsz-in-linux-memory-management/21049737#21049737

4.查看进程的cpu,内存占用情况

top -p pid   #这里可以实时查看cpu和内存

VIRT 虚拟内存
RES 物理内存
5.查看进程的所有指标,(线程,文件句柄fd等)

cat /proc/pid/status

VMPeak虚拟内存峰值
VMSize当前虚拟内存使用量

对于各个内存字段的解释可以参考:http://hutaow.com/blog/2014/08/28/display-process-memory-in-linux/

以上是关于浅谈Android OOM及定位分析的主要内容,如果未能解决你的问题,请参考以下文章

浅谈Android OOM及定位分析

Android oom pthread_create (1040KB stack)分析及解决

拯救OOM 字节自研 Android 虚拟机内存管理优化黑科技 mSponge

浅谈游戏策划定位

JVM的OOM分析总结

使用JVisualVM分析dump文件定位OOM