美文网首页
Android OOM异常分析

Android OOM异常分析

作者: 千里马奇少 | 来源:发表于2020-02-27 09:13 被阅读0次
    OOM目录.png

    什么是OOM?

    OOM全称为OutOfMemoryError,解释为内存溢出,是Android开发中常见的一种错误,这种错误在线上Crash中占比很大一部分,不像NullPointException似的容易定位问题,OOM解决起来相对于一般的Exception或者Error都要难一些,主要是由于错误产生的root cause不是很显而易见。
    在分析OOM之前,先回顾一下Java的内存区域,根据《Java虚拟机规范》的规定,运行时数据区通常包括这几个部分:程序计数器(Program Counter Register)、Java栈(VM Stack)、本地方法栈(Native Method Stack)、方法区(Method Area)、堆(Heap)。


    java内存区域.jpg

    在Java虚拟机规范的描述中,除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OOM的异常可能。

    导致OOM的原因

    Android系统中,OutOfMemoryError这个错误是怎么被系统抛出的?下面基于Android6.0的代码进行简单分析:


    Android源码抛异常处.png
    1. heap.cc是在Android中需要分配的内存大于可用的内存会导致OOM,其最大内存ActivityManager.getMemoryClass()获得,这也是Android中最常见的OOM类型,会抛出异常信息。
    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";
    
    1. thread.cc文件是创建线程时抛出的OOM错误,且有多种错误信息,主要的流程如图所示,有四个点会产生异常信息。


      线程创建过程.png

    创建JNI失败

    创建JNIEnv可以归为两个步骤:
    第一步创建匿名共享内存时,需要打开/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)
    

    创建线程失败

    创建线程也可以归纳为两个步骤:
    第一步分配栈内存失败是由于进程的虚拟内存不足,抛出错误信息如下:

    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)
    

    如何排查OOM

    常用的OOM检测工具介绍:

    工具 问题 能力
    top 内存占用 发现
    meminfo Native内存泄漏,Activity泄漏,数据库缓存命中率 发现+初步定位
    MAT 内存泄漏 发现+定位
    LeakCanary Activity内存泄漏 自动发现+ 定位
    StrictMode Activity内存泄漏 自动发现+ 初步定位
    DDMS 申请内存次数过多、辅助定位GC 发现+ 定位

    在分析清楚OOM产生的原因之后,可以根据堆栈信息的特征来确定这是哪一个类型的OOM,根据以上的异常可以定位产生异常的原因。

    1. 线程数量超出限制
      对于这类异常,是proc/pid/status中记录的线程数(threads项)突破/proc/sys/kernel/threads-max中规定的最大线程数,或者虚拟内存耗尽导致的,通过Thread.getAllStackTraces()可以得到进程中的所有线程以及对应的堆栈信息,就可以定位到到这类线程的创建时机,就能知道问题所在。如果线程是有自定义名称的,那么直接就可以在代码中搜索到创建线程的位置,从而定位问题,如果线程创建时没有指定名称,那么就需要通过该线程的堆栈信息来辅助定位。

    2.FD数量超出限制
    在/proc/pid/limits描述着Linux系统对对应进程的限制,其中Max open files就代表可创建FD的最大数目。进程中创建的FD记录在/proc/pid/fd中,可以得到FD的信息。然后可以查出FD指向的文件,有可能是Socket,File,就可以在代码中定位到出问题的代码。

    3.Java堆溢出
    堆内存分配失败,是Android最常见的OOM,通常说明进程中大部分的内存已经被占用了,且不能被垃圾回收器回收,一般来说此时内存占用都存在一些问题,例如内存泄漏等。要想定位到问题所在,就需要知道进程中的内存都被哪些对象占用,以及这些对象的引用链路。而这些信息都可以在Java内存快照文件中得到,调用Debug.dumpHprofData(String fileName)函数就可以得到当前进程的Java内存快照文件(即HPROF文件),然后可以通过MAT工具进行分析,根据GC root的调用链,找到占用内存的对象,然后定位到代码的位置。

    Android开发中有一个库LeakCanary,可以用来自动分析Activity泄漏,主要的原理是

    • 当一个Activity Destory之后,将它放在一个WeakReference弱引用中中
    • 把这个WeakReference关联到一个ReferenceQueue
    • 查看ReferenceQueue中是否存在Activity的引用
    • 如果Activity泄露了,就Dump出heap信息,然后去分析内存泄露的路径

    如何避免OOM

    在实践中有什么方法来减少OOM的出现呢?总结下来大概分下面几个方面:

    • 减小对象的内存占用
    • 内存对象的重复使用
    • 避免对象的内存泄漏
    • 内存使用策略优化

    减小对象的内存占用

    1. 使用更加轻量的数据结构
      使用ArrayMap/SparseArray替代HashMap等传统数据结构。
      ArrayMap是Android系统专为移动操作系统编写的容器,在大多数情况下,比HashMap效率更高,占用内存更少。
      SparseArray更加高效在于它们避免了对key和value的autobox自动装箱,并且避免了装箱后的解箱。
    2. 避免在Android里面使用Enum
      Android官方说明”Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.“,所以应避免在Android里面使用枚举。
    3. 减小Bitmap对象的内存占用
      Bitmap是一个极容易消耗内存的大胖子,减小创建处理的Bitmap的内存占用是很重要的,通常来说有下面2个措施:
      inSampleSize:缩放比例,在把图片载入内存之前,我们需要先计算出一个合适的缩放比例,避免不必要的大图载入。
      decode format:解码格式,选择ARGB_8888/RGB_565/ARGB_4444/ALPHA_8,存在很大差异。
    4. 使用更小的图片
      对应资源图片,要特别留意这张图片是否存在可压缩的空间,是否可以使用一张更小的图片。尽量使用更小的图片不仅仅可以减少内存的使用,还可以避免出现大量的InflationException。假设有一张很大的图片被XML文件直接引用,很有可能在初始化视图的时候会因为内存不足而发生InflationException,这个问题的根本原因其实是发生了OOM。

    内存对象的重复使用

    1. 注意在ListView/GridView等出现大量重复子组件的视图里面对ConvertView的复用
    2. Bitmap对象的复用
      在RecyclerView、ListView、GridView等显示大量图片的控件里面需要使用LRU机制来缓存处理好的Bitmap。
      利用inBitmap的高级特性提高Android系统在Bitmap分配与释放执行效率上的提升。
    3. 避免在onDraw方法里面执行对象的创建
      类似onDraw等频繁调用的方法,一定需要注意避免在这里做创建对象的操作,因为它会迅速增加内存的使用,而且很容易引起频繁的GC,甚至是内存抖动。
    4. StringBuilder
      当代码中需要使用到大量的字符串拼接操作,就有必要考虑使用StringBuilder来代替频繁的”+“。

    避免对象的内存泄漏

    1. 注意Activity的泄漏
      通常来说,Activity的泄漏是内存泄漏里面最为严重的问题,它占用的内存最多,影响面广。
      导致Activity泄漏的两种情况:
      内部类引用导致Activity的泄漏
      Activity Context被传递到其他实例中,这可能导致自身被引用而发生泄漏。
    2. 考虑使用Application Context而不是Activity Context
      对于大部分非必须使用Activity Context的情况(Dialog的Context就必须是Activity Context),都可以考虑使用Application Context而不是Activity的Context,这样就可以避免不经意的Activity泄漏。
    3. 注意临时Bitmap对象的及时回收
      临时创建的某个相对比较大的bitmap对象,在经过转换得到新的bitmap对象之后,应该尽快回收原始的bitmap,这样能够更快释放原始bitmap所占用的空间。
    4. 注意监听器的注销
      在Android程序里面存在很多需要register和unregister的监听器,需要确保在合适的时候及时unregister那些监听器。手动add的listener,需要记得及时remove这个listener。
    5. 注意缓存容器中的对象泄漏
      如果容器是静态或者全局的,那么对于里面存放的对象要及时remove。
    6. 注意WebView的泄漏
      Android中WebView存在很大的兼容性问题,需要再合适的时机进行销毁。
    7. 注意Cursor对象是否及时关闭
      对于数据库查询的Cursor,如果没有及时关闭就会造成泄漏。

    内存使用策略优化

    1. Try catch 某些大内存的操作
      在某些情况下,我们需要事先评估那些可能发生OOM的代码,对于这些可能发生OOM的代码,加入catch机制,可以考虑在catch里面尝试一次降级的内存分配操作。例如decode bitmap的时候,catch到OOM,可以尝试把采样比例再增加一倍之后,再次尝试decode。
    2. 谨慎使用static 对象
      static是Java中的一个关键字,当用它来修饰成员变量时,那么该变量就属于该类,而不是该类的实例。 不少程序员喜欢用static这个关键字修饰变量,因为他使得变量的生命周期大大延长啦,并且访问的时候,也极其的方便,用类名就能直接访问,各个资源间 传值也极其的方便,所以,它经常被我们使用。但如果用它来引用一些资源耗费过多的实例(Context的情况最多),这时就要谨慎对待了。
    3. 特别留意单例模式的不合理持有
    4. 优化布局层次,减少内存消耗
      越扁平化的视图布局,占用的内存就越少,效率越高。我们需要尽量保证布局足够扁平化,当使用系统提供的View无法实现足够扁平的时候考虑使用自定义View来达到目的。

    参考链接

    Android OOM 分析
    Probe:Android线上OOM问题定位组件

    相关文章

      网友评论

          本文标题:Android OOM异常分析

          本文链接:https://www.haomeiwen.com/subject/bmwcqhtx.html