Android系统的内存与崩溃优化

Android优化

题记:当你看到一堆让你摸不着头脑的崩溃堆栈,夹杂着若干OOM崩溃的时候,那就是在告诉你——是时候优化内存了。

最近这段时间一直在跟进安卓崩溃的问题,跟了有三个月了,虽然有一些进展,但是目前也还没有彻底解决,想起自己已经有一年没有写过博客了,所以打算把最近学到的东西,都好好整理一下。

看到这个标题,就能知道我最近解决的崩溃问题都是内存导致的了,内存问题相比于普通的崩溃难度要高一些,因为内存崩溃的堆栈都不能直接反馈问题,有些只是压死骆驼的最后一根稻草,崩溃点也是五花八门。所以要解决内存的问题,首先就要对内存有一个全面切清晰的认识,你才知道要从哪里入手。

内存的相关概念——弄懂大Boss

  • 虚拟内存和物理内存

    物理内存,顾名思义,就是实际分配到内存条中的内存;虚拟内存,也称逻辑内存,是存在于数据结构中的。在32位操作系统中,Linux进程的虚拟内存大小是4G,32位系统的最大寻址空间大小正好是4G(2^32)。在这4G里面,其中1G是内核态内存,每个Linux进程共享这一块内存,在高地址;3G是用户态内存,这部分内存进程间不共享,在低地址。内存的分布图如下:

而用户态又被切分为很多个部分,从低地址往上分别是:

  1. Text Segment :存放二进制可执行代码的位置;

  2. Data Segment: 存放静态常量;

  3. BSS Segment :存放未初始化的静态变量;

  4. Heap:堆是往高地址增长的,是用来动态分配内存的区域,malloc 就是在这里面分配的;

  5. Memory Mapping Segment:这块地址可以用来把文件映射进内存用的,如果二进制的执行文件依赖于某个动态链接库,就是在这个区域里面将 so 文件映射到了内存中;

  6. Stack:主线程的函数调用的函数栈就是用这里的。

    虚拟内存的分布,在进程中是有数据结构来记录的,所有进程内申请内存的动作,无论是java的new,还是c++的malloc,首先都是申请的虚拟内存,要等到实际发生内存访问的时候,会发生缺页中断,然后在物理内存中分配内存。

这个步骤比较繁琐,涉及的知识点比较多,后面我会整理这中间学到的Linux内存相关知识,再给大家分享。

  • Dalvik内存和native内存

    要搞清楚这两个概念,首先要搞清楚Dalvik和Linux的关系。也就是说,每个安卓上的进程,都会启动一个Dalvik虚拟机,Java申请的内存,就是Linux进程可以分配给Dalvik虚拟机的内存,安卓系统内有一项配置:dalvik.vm.heapsize,定义了这台手机上每个dalvik能申请的最大内存,也就是系统能给虚拟机分配的最大内存。高端的手机一般能分配到512M的最大内存,低端机一般是256M。

    而Native内存,是脱离于Dalvik虚拟机,直接在系统层面申请的内存,只要虚拟内存和物理内存没用完,就能一直申请,这也是为什么,android 4.x之后,Bitmap的内存会直接在native中分配的缘故,就是因为看上了Native比Dalvik大。

  • VSS, USS, PSS和RSS

这几个概念,是另外一套用来描述进程内存现状的,要理解这几个概念,首先要理解系统内存的分配方式,也就是虚拟内存是通过什么样的机制分配到物理内存中去的。在android系统中,使用的分配方式是分页,这也是Linux内最常用的分配内存方式。

简单来说,分页就是将系统内存分成一页页,每页的内存大小是4K,然后系统会跟踪所有的内存页面,如下图:

image.png

在确定应用使用的内存量时,系统必须考虑共享的页面。访问相同服务或库的应用将共享内存页面。例如,Google Play 服务和某个游戏应用可能会共享位置信息服务。这样便很难确定属于整个服务和每个应用的内存量分别是多少。

image.png

图 6. 由两个应用共享的页面(中间)

如需确定应用的内存占用量,可以使用以下任一指标:

  1. 常驻内存大小 (RSS):应用使用的共享和非共享页面的数量
  2. 按比例分摊的内存大小 (PSS):应用使用的非共享页面的数量加上共享页面的均匀分摊数量(例如,如果三个进程共享 3MB,则每个进程的 PSS 为 1MB)
  3. 独占内存大小 (USS):应用使用的非共享页面数量(不包括共享页面)

如果操作系统想要知道所有进程使用了多少内存,那么 PSS 非常有用,因为页面只会统计一次。计算 PSS 需要花很长时间,因为系统需要确定共享的页面以及共享页面的进程数量。RSS 不区分共享和非共享页面(因此计算起来更快),更适合跟踪内存分配量的变化。

查看PSS的方式比较简单,安卓有直接的adb命令可以查看,命令如下:

dengzongrongdeMacBook-Pro-3:~ RoyDeng$ adb shell dumpsys meminfo com.dianyun.pcgo
Applications Memory Usage (in Kilobytes):
Uptime: 407897842 Realtime: 816330794

** MEMINFO in pid 11779 [com.dianyun.pcgo] **
                   Pss  Private  Private  SwapPss     Heap     Heap     Heap
                 Total    Dirty    Clean    Dirty     Size    Alloc     Free
                ------   ------   ------   ------   ------   ------   ------
  Native Heap   120337   120164        0      298   157172   148680     8491
  Dalvik Heap    32207    32164        0       57    41412    20706    20706
 Dalvik Other     8297     8296        0        0
        Stack       84       84        0        0
       Ashmem      158       72        0        0
      Gfx dev    26632    26632        0        0
    Other dev      204        0      204        0
     .so mmap    68401     3140    54180       12
    .jar mmap     4384        0     1220        0
    .apk mmap     2751      152     1516        0
    .ttf mmap     4402        0     1172        0
    .dex mmap    66430    55992    10096        0
    .oat mmap     2480        0      516        0
    .art mmap     5450     4680        0       33
   Other mmap    12593     1396     7544        0
      Unknown    12874    12852        0        8
        TOTAL   368092   265624    76448      408   198584   169386    29197

 App Summary
                       Pss(KB)
                        ------
           Java Heap:    36844
         Native Heap:   120164
                Code:   127984
               Stack:       84
            Graphics:    26632
       Private Other:    30364
              System:    26020

               TOTAL:   368092       TOTAL SWAP PSS:      408
  • 查看进程内存分布

Linux将进程的内存分布写在一个文件里面,可以通过以下命令查看内存情况:

$ cat /proc/[pid]/maps

其中vm_size这一行,就是表示进程的虚拟内存大小,这个数据目前只能通过读取/proc/[pid]/maps文件来获得,是我们在内存优化中,非常重要的一项指标。

OOM崩溃——第一只拦路虎

说到内存问题,第一想到的应该就是OOM崩溃了。以前我碰到OOM问题都会选择性过滤,认为这个崩溃没那么好解,要等到后续专门针对内存做统一优化;现在,能碰到OOM,真是一种幸福。

我们碰到的OOM崩溃主要有以下两种:

  1. 832503 java.lang.OutOfMemoryError: Failed to allocate a 9936012 byte allocation with 4745096 free bytes and 4MB until OOM

  2. 269867 java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Try again

第一种崩溃比较简单,原因就是给dalvik分配的最大内存超过限制了,这块内存分配不了,然后系统就报错了。解法有两种:

  1. 简单解法:一般报错的地方都是申请内存比较频繁或者比较大的地方,可以直面这个堆栈,想办法绕过去,比如说把一张大图用.9图来代替等等。

    不过这个方法有个坏处,就是不彻底,因为有可能堆栈反应的问题,只是压死骆驼的最后一根稻草,要想彻底解决这个问题,安卓提供了一个非常好的工具,就是hprof。

  2. 彻底解法:就是想办法拿到崩溃场景下的hprof文件,这样就能看到崩溃前到底是什么东西占用着内存。因为导出hprof需要时间,并且dump的过程中app会被冻结,所以在用户环境上不是很好导出。最好的方法,是观察用户行为日志,一边借助内存检测工具(我们使用的是perfDog),来重现崩溃场景。

    快手最近有开源一款解决dump过程中app冻结的内存导出方案——KOOM,我们还没有在项目中实际投入使用,有兴趣的可以了解一下:https://github.com/KwaiAppTeam/KOOM

第二种崩溃,是java在创建线程的时候发生的崩溃。从堆栈可以看出,崩溃点发生在native层,所以和虚拟机内存限制无关。通过在网上查资料——《Android 创建线程源码与OOM分析》,发现出现这个问题的原因有两种,一种是虚拟内存不足,第二种是文件描述符超出限制,究竟是因为哪个呢?

为了定位这个问题,我们做了一套崩溃收集方案,目的不是为了收集崩溃,而是收集崩溃时候的机器状态,包括内存、线程、文件描述符、activityStack等等,在灰度的过程中,我们也在看其他的那些比oom更加棘手的问题。

乱七八糟的Native崩溃——真正的大Boss

OOM问题,有简单的,有复杂的,不过真正把我们难倒的,不是OOM,而是一堆乱七八糟的native崩溃,其中TOP1崩溃的堆栈,看上去很短,但是很让人摸不着头脑:

1   #00 pc 00056c62 /apex/com.android.runtime/lib/bionic/libc.so (abort+165) [armeabi-v8]
2   #01 pc 00005aad /system/lib/liblog.so (__android_log_assert+176) [armeabi-v8]
3   #02 pc 001f78f7 /system/lib/libhwui.so [armeabi-v8]
4   #03 pc 001f6abd /system/lib/libhwui.so [armeabi-v8]
5   #04 pc 001f6011 /system/lib/libhwui.so [armeabi-v8]
6   #05 pc 002041e9 /system/lib/libhwui.so [armeabi-v8]
7   #06 pc 00204041 /system/lib/libhwui.so [armeabi-v8]
8   #07 pc 0000da1f /system/lib/libutils.so (android::Thread::_threadLoop(void*)+214) [armeabi-v8]
9   #08 pc 000a109b /apex/com.android.runtime/lib/bionic/libc.so (__pthread_start(void*)+20) [armeabi-v8]
10 #09 pc 00058113 /apex/com.android.runtime/lib/bionic/libc.so (__start_thread+30) [armeabi-v8]

崩溃的原因是abort,一般出现这种问题的原因,是系统没办法了,然后自杀了,而这个没办法的原因,让我猜到了和内存可能有关,但是不能确定。通过客户端收集到的log,也没办法定位到问题,因为客户端只收集自己打印的log。眼看就要没办法之际,一次偶然的机会,看到了bugly上收集到的短暂的logcat日志,竟然每一条都有这样同样的log输出:

10-24 00:05:20.046 13795 14043 W Adreno-GSL: : sharedmem_gpumem_alloc: mmap failed errno 12 Out of memory
10-24 00:05:20.048 13795 14043 E Adreno-GSL: : GSL MEM ERROR: kgsl_sharedmem_alloc ioctl failed.
10-24 00:05:20.055 13795 14043 W Adreno-GSL: : sharedmem_gpumem_alloc: mmap failed errno 12 Out of memory
10-24 00:05:20.056 13795 14043 E Adreno-GSL: : GSL MEM ERROR: kgsl_sharedmem_alloc ioctl failed.
10-24 00:05:20.057 13795 14043 W Adreno-GSL: : sharedmem_gpumem_alloc: mmap failed errno 12 Out of memory
10-24 00:05:20.058 13795 14043 E Adreno-GSL: : GSL MEM ERROR: kgsl_sharedmem_alloc ioctl failed.
10-24 00:05:20.059 13795 14043 E OpenGLRenderer: GL error: Out of memory!

得嘞,问题就是你了,GL error:Out of Memory,对应的代码是在这里:

bool GLUtils::dumpGLErrors() {
    bool errorObserved = false;
    GLenum status = GL_NO_ERROR;
    while ((status = glGetError()) != GL_NO_ERROR) {
        errorObserved = true;
        switch (status) {
            case GL_INVALID_ENUM:
                ALOGE("GL error:  GL_INVALID_ENUM");
                break;
            case GL_INVALID_VALUE:
                ALOGE("GL error:  GL_INVALID_VALUE");
                break;
            case GL_INVALID_OPERATION:
                ALOGE("GL error:  GL_INVALID_OPERATION");
                break;
            case GL_OUT_OF_MEMORY:
                ALOGE("GL error:  Out of memory!");
                break;
            default:
                ALOGE("GL error: 0x%x", status);
        }
    }
    return errorObserved;
}

从上面的创建线程OOM和GL OOM两个问题,已经大概猜到这次碰到的问题就是内存问题了,但是要证明是内存问题,还是需要有证据。对此,我们的方法是在崩溃回调的接口中,搜集当时的数据,包括java内存、native内存、虚拟内存、文件描述符、线程数等等,搜集到的数据会在log中打印,并且上报给崩溃后台,通过收集一轮数据,我们发现我们的猜想是正确的:崩溃的原因,就是虚拟内存不足导致,同文件描述符、线程数、java内存大小等其他指标无关。

GL OOM——挑战大Boss

不管是虚拟内存还是物理内存,要解决内存问题的思路都是一样的:首先就是要找到问题出在哪,只要找到了问题根源,解决问题就不是一件麻烦事。但是问题就在于,怎么样才能找到内存问题的大头在哪呢?为此,我们做了一套比较完整的内存交控方案,具体策略如下:

  1. 在Activity跳转的过程中添加内存信息打印,打印的时间点包括:

    1. Activity.onStart()
    2. Activity.onStop()
    3. 在同一个Activity的停留时间每超过一分钟

    这样我们就得到了一份内存数据增长曲线,通过收集内存问题崩溃用户的log,最终定位到有三个Activity有严重的内存泄露问题

  2. 我们碰到的内存问题都是偶现问题,所以要想解决问题,同时验证解决方案是否生效,必须要做的一件事情就是想办法重现问题。所以我们开发同学配合测试同学,一起搭建了一套自动化测试框架,辅助性能分析工具perfDog,成功找出了问题所在。

找到了问题根源,解决问题就简单很多,因为涉及到项目问题,在这里就不透露解决问题的方案。

问题的原因主要有两点:

  1. 在我们的业务场景中,有一个场景,用户会频繁连接和断开音视频直播流,这个过程每发生一次,就会造成20M的内存泄露。下面是自动化脚本模拟该业务场景的内存增长曲线:


    重连直播流内存泄漏
  1. X5框架内存在内存堆积,堆积点是用户退出网页之后,jsapi会继续持有X5WebViewAdapter,继而持有Activity,导致内存堆积,堆栈如下:


    X5内存泄露

知道了上面两个问题,解决方案就可以顺藤摸瓜了:

  1. 直播流内存泄露问题,找到了原因是很多析构函数没有找到,问题已修复;
  2. web内存堆积问题,解决方案是做web子进程改造,这也是微信采用的解决方案,目前正在开发中。

经验分享

经过这次崩溃优化,自己总结了一些方法论,如果要解决内存相关的崩溃问题,要做的事情分三步:

  1. 确认崩溃和内存相关。这一步需要开发者有扎实的理论功底,能够理解安卓内存模型,并且要通过分析崩溃堆栈,得到该崩溃是否是内存问题的结论。
  2. 找到内存问题原因。这一步需要开发者有侦察能力和足够的耐心,能够通过log发现用户操作的共性,然后谨慎的重现问题,只要重现了问题,后面的事情就好办了。这一步也是最重要的一步
  3. 优化内存问题点。这一步需要开发者有丰富的开发经验,要得出最小成本的解决方案。

以上就是我在最近崩溃优化中所总结出来的一些经验,感谢您的阅读,希望能对你有帮助,有问题欢迎留言讨论!

相关链接:
native 内存和 dalvik内存
进程间的内存分配
KOOM——高性能线上内存监控方案
Android 创建线程源码与OOM分析

你可能感兴趣的:(Android系统的内存与崩溃优化)