最近排查了一个android虚拟内存OOM的问题,记录一下排查解决过程
背景
最近两个版本线上的 OOM 数量猛增,基本上排名前几的Crash 都是OOM。
其中有个堆栈如下:
java.lang.OutOfMemoryError
pthread_create (1040KB stack) failed: Try again java.lang.Thread.nativeCreate(Native Method) java.lang.Thread.start(Thread.java:753) java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:975) java.util.concurrent.ThreadPoolExecutor.processWorkerExit(ThreadPoolExecutor.java:1043) java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1159) java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641) java.lang.Thread.run(Thread.java:784)
|
看到这个 pthread_create
首先想到的是 FD
泄漏,看了下崩溃记录,剩余 FD
还有很多(之前查过一个严重的 FD
问题,所以 bugly 上自己加上了 FD
的信息),然后剩余的应用内存也有很多,根据之前的经验这应该不是一个常规的 OOM 问题,搜索了下资料,确定了是虚拟内存泄漏:
崩溃机型都是32位的,CPU架构为 armeabi-v7a
log 中有关键字 VmSize
,与 Thread
的 native 源码报错一致,CursorWindow
的报错代码 -12 (忘记在哪看到的了)
libc : pthread_create failed: couldn't allocate 1073152-bytes mapped space: Out of memory Throwing OutOfMemoryError with VmSize 4183816 kB "pthread_create (1040KB stack) failed: Try again" ... CursorWindow: CursorWindow MAP_FAILED result=-12, errno=12 CursorWindow: Could not allocate CursorWindow '/data/user/0/***/databases/*.db' of size 2097152 due to error -12.
|
bugly 中上报的 VmPeak
(虚拟内存峰值)的确超过了 3G,很多占用超 4G 的
排查过程
既然确定是虚拟内存的问题,查了很多资料看虚拟内存究竟是什么(好像不重要,知道是android有个限制,触发了会OOM就可以了),然后想复现一下问题。
找32位设备就花了很大力气,公司的低端机都是64位了,于是看了下 WeTest
,32位机型倒是挺多的,但是收费60分钟要40块,调试几小时那不是几百块没了,想了下我微薄的工资,还是算了?
放弃之后灵机一动想到之前的虚拟机不都是32位的吗,于是愉快的打开 Genymotion 、安装 app、启动,过一会就崩溃了,跟bugly上报的日志一样,所以这是一个必现的问题???
重新打开 app 试了几次,都能必现 crash,查看 log 都跟上面的特征符合。
接下来我需要观察 app 启动之后虚拟内存的变化,写了个脚本如下:
import time import os
package_name = "应用包名" os.popen("adb shell am force-stop " + package_name) time.sleep(2) os.popen("adb shell am start -n " + package_name+"/LauncherActivity路径") time.sleep(2) pid = os.popen("adb shell pidof -s " + package_name).read().strip()
while True:
if len(pid)==0: break
# 获取该应用的内存信息 mem_info = os.popen(f"adb shell 'cat proc/{pid}/status' | grep -E 'VmSize|VmPeak'").read() if "VmSize" in mem_info: print(mem_info) else: break # 每隔100毫秒打印一次 time.sleep(0.1)
|
输出结果:
VmPeak: 2098856 kB VmSize: 2098856 kB ... ...
VmPeak: 2412676 kB VmSize: 2412676 kB
VmPeak: 2489512 kB VmSize: 2489512 kB
VmPeak: 2568568 kB VmSize: 2568568 kB
VmPeak: 2648744 kB VmSize: 2648744 kB
VmPeak: 2734440 kB VmSize: 2734440 kB
VmPeak: 2814576 kB VmSize: 2814576 kB
VmPeak: 2893072 kB VmSize: 2893072 kB
VmPeak: 2965360 kB VmSize: 2965360 kB
VmPeak: 3047288 kB VmSize: 3047288 kB
VmPeak: 3129056 kB VmSize: 3129056 kB
VmPeak: 3214804 kB VmSize: 3214804 kB
VmPeak: 3299028 kB VmSize: 3299028 kB
VmPeak: 3381748 kB VmSize: 3381748 kB
VmPeak: 3462964 kB VmSize: 3462964 kB
VmPeak: 3547188 kB VmSize: 3547188 kB
VmPeak: 3624380 kB VmSize: 3624380 kB
VmPeak: 3706192 kB VmSize: 3706192 kB
VmPeak: 3787780 kB VmSize: 3787780 kB
VmPeak: 3870460 kB VmSize: 3870460 kB
VmPeak: 3954412 kB VmSize: 3954412 kB
VmPeak: 4025940 kB VmSize: 4025940 kB
VmPeak: 4058964 kB VmSize: 4058964 kB
VmPeak: 4105920 kB VmSize: 4105920 kB
VmPeak: 4139976 kB VmSize: 4139976 kB
VmPeak: 4189604 kB VmSize: 4189604 kB ...
|
观察虚拟内存变化可以发现 app 启动之后虚拟内存稳定在 2g 左右,然后在两秒左右的时间内飙升至4G,导致应用 crash
接下来仔细观察 crash 时的 logcat 日志,多尝试几遍之后,发现有一处文件重复引用非常多,比较可疑,于是找到该业务的入口屏蔽之后重新打包,观察虚拟内存可以稳定在 2g 左右,到这里基本就确定问题了。
该业务的伪代码如下:
private void updateZip(){ File[] localFiles = localDir.listFile(); for (i = 0; i < localFiles.length(); i++) { for (j = 0; j < serverDatas.size(); j++) { if (FileUtil.checkMd5(local, server.md5)) { ... } } } }
|
这段代码有两处问题:
函数体中第一行的 listFile()
拿到文件列表之后会一直持有文件列表的引用直到函数执行完毕,把这个地方改成 File
中的 String[].list()
只返回一个 String
列表,需要文件的时候再去 new File
。改完之后测试虚拟内存发现峰值只有 3g 左右,应用已经不再崩溃
FileUtil.checkMd5
这个方法每次都会读文件流,此时服务端返回的 serverDatas
已经有 300 个 item,也就是说这段代码在一瞬间最多可能执行 9 万次文件流读取,所以虚拟内存才会爆表。
改法也很简单,把 FileUtil.checkMd5
移到外层 或者加入读文件 MD5 的缓存都可以
两处一起修改完毕之后,观察虚拟内存的峰值可以稳定在 2g 多一点,到这里就解决问题了
参考链接: