题记:当你看到一堆让你摸不着头脑的崩溃堆栈,夹杂着若干OOM崩溃的时候,那就是在告诉你——是时候优化内存了。
最近这段时间一直在跟进安卓崩溃的问题,跟了有三个月了,虽然有一些进展,但是目前也还没有彻底解决,想起自己已经有一年没有写过博客了,所以打算把最近学到的东西,都好好整理一下。
看到这个标题,就能知道我最近解决的崩溃问题都是内存导致的了,内存问题相比于普通的崩溃难度要高一些,因为内存崩溃的堆栈都不能直接反馈问题,有些只是压死骆驼的最后一根稻草,崩溃点也是五花八门。所以要解决内存的问题,首先就要对内存有一个全面切清晰的认识,你才知道要从哪里入手。
内存的相关概念——弄懂大Boss
-
虚拟内存和物理内存
物理内存,顾名思义,就是实际分配到内存条中的内存;虚拟内存,也称逻辑内存,是存在于数据结构中的。在32位操作系统中,Linux进程的虚拟内存大小是4G,32位系统的最大寻址空间大小正好是4G(2^32)。在这4G里面,其中1G是内核态内存,每个Linux进程共享这一块内存,在高地址;3G是用户态内存,这部分内存进程间不共享,在低地址。内存的分布图如下:
而用户态又被切分为很多个部分,从低地址往上分别是:
Text Segment :存放二进制可执行代码的位置;
Data Segment: 存放静态常量;
BSS Segment :存放未初始化的静态变量;
Heap:堆是往高地址增长的,是用来动态分配内存的区域,malloc 就是在这里面分配的;
Memory Mapping Segment:这块地址可以用来把文件映射进内存用的,如果二进制的执行文件依赖于某个动态链接库,就是在这个区域里面将 so 文件映射到了内存中;
-
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,然后系统会跟踪所有的内存页面,如下图:
在确定应用使用的内存量时,系统必须考虑共享的页面。访问相同服务或库的应用将共享内存页面。例如,Google Play 服务和某个游戏应用可能会共享位置信息服务。这样便很难确定属于整个服务和每个应用的内存量分别是多少。
图 6. 由两个应用共享的页面(中间)
如需确定应用的内存占用量,可以使用以下任一指标:
- 常驻内存大小 (RSS):应用使用的共享和非共享页面的数量
- 按比例分摊的内存大小 (PSS):应用使用的非共享页面的数量加上共享页面的均匀分摊数量(例如,如果三个进程共享 3MB,则每个进程的 PSS 为 1MB)
- 独占内存大小 (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崩溃主要有以下两种:
832503 java.lang.OutOfMemoryError: Failed to allocate a 9936012 byte allocation with 4745096 free bytes and 4MB until OOM
269867 java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Try again
第一种崩溃比较简单,原因就是给dalvik分配的最大内存超过限制了,这块内存分配不了,然后系统就报错了。解法有两种:
-
简单解法:一般报错的地方都是申请内存比较频繁或者比较大的地方,可以直面这个堆栈,想办法绕过去,比如说把一张大图用.9图来代替等等。
不过这个方法有个坏处,就是不彻底,因为有可能堆栈反应的问题,只是压死骆驼的最后一根稻草,要想彻底解决这个问题,安卓提供了一个非常好的工具,就是hprof。
-
彻底解法:就是想办法拿到崩溃场景下的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
不管是虚拟内存还是物理内存,要解决内存问题的思路都是一样的:首先就是要找到问题出在哪,只要找到了问题根源,解决问题就不是一件麻烦事。但是问题就在于,怎么样才能找到内存问题的大头在哪呢?为此,我们做了一套比较完整的内存交控方案,具体策略如下:
-
在Activity跳转的过程中添加内存信息打印,打印的时间点包括:
- Activity.onStart()
- Activity.onStop()
- 在同一个Activity的停留时间每超过一分钟
这样我们就得到了一份内存数据增长曲线,通过收集内存问题崩溃用户的log,最终定位到有三个Activity有严重的内存泄露问题
我们碰到的内存问题都是偶现问题,所以要想解决问题,同时验证解决方案是否生效,必须要做的一件事情就是想办法重现问题。所以我们开发同学配合测试同学,一起搭建了一套自动化测试框架,辅助性能分析工具perfDog,成功找出了问题所在。
找到了问题根源,解决问题就简单很多,因为涉及到项目问题,在这里就不透露解决问题的方案。
问题的原因主要有两点:
-
在我们的业务场景中,有一个场景,用户会频繁连接和断开音视频直播流,这个过程每发生一次,就会造成20M的内存泄露。下面是自动化脚本模拟该业务场景的内存增长曲线:
-
X5框架内存在内存堆积,堆积点是用户退出网页之后,jsapi会继续持有X5WebViewAdapter,继而持有Activity,导致内存堆积,堆栈如下:
知道了上面两个问题,解决方案就可以顺藤摸瓜了:
- 直播流内存泄露问题,找到了原因是很多析构函数没有找到,问题已修复;
- web内存堆积问题,解决方案是做web子进程改造,这也是微信采用的解决方案,目前正在开发中。
经验分享
经过这次崩溃优化,自己总结了一些方法论,如果要解决内存相关的崩溃问题,要做的事情分三步:
- 确认崩溃和内存相关。这一步需要开发者有扎实的理论功底,能够理解安卓内存模型,并且要通过分析崩溃堆栈,得到该崩溃是否是内存问题的结论。
- 找到内存问题原因。这一步需要开发者有侦察能力和足够的耐心,能够通过log发现用户操作的共性,然后谨慎的重现问题,只要重现了问题,后面的事情就好办了。这一步也是最重要的一步。
- 优化内存问题点。这一步需要开发者有丰富的开发经验,要得出最小成本的解决方案。
以上就是我在最近崩溃优化中所总结出来的一些经验,感谢您的阅读,希望能对你有帮助,有问题欢迎留言讨论!
相关链接:
native 内存和 dalvik内存
进程间的内存分配
KOOM——高性能线上内存监控方案
Android 创建线程源码与OOM分析