0%

记一次android虚拟内存OOM排查过程

最近排查了一个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++) {
//将本地文件MD5与服务端MD5进行对比
if (FileUtil.checkMd5(local, server.md5)) {
...
}
}
}
}

这段代码有两处问题:

  1. 函数体中第一行的 listFile() 拿到文件列表之后会一直持有文件列表的引用直到函数执行完毕,把这个地方改成 File 中的 String[].list() 只返回一个 String 列表,需要文件的时候再去 new File 。改完之后测试虚拟内存发现峰值只有 3g 左右,应用已经不再崩溃

  2. FileUtil.checkMd5 这个方法每次都会读文件流,此时服务端返回的 serverDatas 已经有 300 个 item,也就是说这段代码在一瞬间最多可能执行 9 万次文件流读取,所以虚拟内存才会爆表。

    改法也很简单,把 FileUtil.checkMd5 移到外层 或者加入读文件 MD5 的缓存都可以

两处一起修改完毕之后,观察虚拟内存的峰值可以稳定在 2g 多一点,到这里就解决问题了

参考链接: