0%

线程导致的OOM问题定位及解决

在某个版本,上报平台新增了一些OOM的问题。

1. 分析闪退日志

1
2
3
4
5
6
7
8
9
10
11
java.lang.OutOfMemoryError

Could not allocate JNI Env

java.lang.Thread.nativeCreate(Native Method)
2 java.lang.Thread.start(Thread.java:730)
3 java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:941)
4 java.util.concurrent.ThreadPoolExecutor.processWorkerExit(ThreadPoolExecutor.java:1009)
5 java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1151)
6 java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)
7 java.lang.Thread.run(Thread.java:761)

从堆栈分析,线程导致的OOM是线程创建导致的,我们分析手机内存大小,发现内存并不紧张。

2. 本地验证

for循环创建大量的线程,可以复现这个问题。

网上查阅资料,发现部分手机厂商将进程创建的线程个数进行了限制,这些手机的线程数限制都很小(应该是华为rom特意修改的limits),每个进程只允许最大同时开500个线程,因此很容易复现。

3. 问题定位

3.1 本地监控线程的创建,检查是否是代码不合理导致的线程创建过多。
方案1:通过代码插桩实现线程创建监控 这类操作可以参考didi出的两套框架 boosterDroidAssist
方案2:采用hook的方案监控线程创建。SandHook

这里我采用的是第二种方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
Constructor<?>[] constructors = Thread.class.getConstructors();
for (Constructor constructor : constructors) {
Class[] types = constructor.getParameterTypes();

int threadName = -1;
for (int i = 0; i < types.length; i++) {
if (types[i] == String.class) {
threadName = i;
break;
}
}
int finalThreadName = threadName;
XC_MethodHook xc_methodHook = new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
threadNum ++;
if (finalThreadName >= 0){
LogUtil.e(TAG, "created thread: " + threadNum + "-->" + param.args[finalThreadName]);
} else {
LogUtil.e(TAG, "created thread: " + threadNum);
}
LogUtil.e(TAG, " is created." + LogUtil.getStack());
}

@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);

}
};
Object[] params = new Object[types.length + 1];
System.arraycopy(types, 0, params, 0, types.length);
params[types.length] = xc_methodHook;
XposedHelpers.findAndHookConstructor(Thread.class, params);
}

通过监控代码的创建,我们没有发现本地有大量创建线程的逻辑。无法复现场景,但是hook框架我们没办法放到正式环境,在正式环境我们采用了另外一种方案。

4. 现网问题定位

现在的bug上报平台都支持扩展信息,以bugly为例,在扩展信息带上当前进程的线程数和名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
strategy.setCrashHandleCallback

if ("java.lang.OutOfMemoryError".equals(errorType)) {
ThreadGroup group = Thread.currentThread().getThreadGroup();
ThreadGroup topGroup = group;
// 遍历线程组树,获取根线程组
while (group != null) {
topGroup = group;
group = group.getParent();
}
// 激活的线程数再加一倍,防止枚举时有可能刚好有动态线程生成
int slackSize = topGroup.activeCount() * 2;
Thread[] threads = new Thread[slackSize];

// 获取根线程组下的所有线程,返回的actualSize便是最终的线程数
int threadSize = topGroup.enumerate(threads);
stringBuilder.append("thread count: ").append(threadSize).append("\n");

for (int i = 0; i < threadSize; i++) {
Thread thread = threads[i];
if (thread != null) {
stringBuilder.append(thread.getName()).append("\n");
}
}
}

我们在上线以后,我们通过线程名,发现了使用一个第三方sdk不合理导致线程大量创建的问题,修复以后的版本再无这个OOM的问题。