Link在华为手机上有一些很高的Crash,原因是RxJava调用不当导致的。
一.问题描述
Link有大量因为OOM引起的Crash,日志上总体表现为 pthread_create (1040KB stack) failed: Out of memory
,集中发生在Android6.0及以上的华为手机。
二.问题分析
2.1 代码分析
Android系统中,OutOfMemoryError这个错误是怎么被系统抛出的?基于Android6.0的代码可以看到:
Android虚拟机最终抛出OutOfMemoryError
的代码位于 /art/runtime/thread.cc
void Thread::ThrowOutOfMemoryError(const char* msg)
参数msg携带了OOM时的错误信息。搜索代码可以看到异常在下面这个函数抛出:
void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon)
同时会打出与我们问题一致的错误信息:
StringPrintf("pthread_create (%s stack) failed: %s", PrettySize(stack_size).c_str(), strerror(pthread_create_result)))
由此可以定位到问题 -- 创建线程时导致了OOM。那么,为什么华为手机会在创建线程时OOM呢?
2.2 推断
既然抛出OOM,一定是触发了系统线程限制,Android基于Linux,所以在 /proc/sys/kernel/threads-max 中有描述线程限制,可以通过命令cat /proc/sys/kernel/threads-max
查看。我们分别查看华为Mate7和小米Note的线程限制。
Mate7
shell@hwmt7:/ $ cat /proc/sys/kernel/threads-max
26599
MiNote
shell@virgo:/ $ cat /proc/sys/kernel/threads-max
39595
我们可以发现,华为在线程限制上非常严苛,Mate7的最大线程数远远小于小米Note,所以导致单华为机型爆发Crash。那么是哪里代码导致了线程爆发呢?
我们通过Fabric任务栈可以发现,这几个Crash爆发时,线程数量都在400+,而且有大量RxIoSchedule线程处于wait状态,可以推断出RxJava调度器Scheduler.io中维护的线程池没起作用。
2.3 验证
我们先对上面的推断做一个本地实验:试图复现错误信息一致的OOM。
- 过程:用Rxjava在IO调度器中创建大量线程模仿网络请求,看是否会回收。
- 预期:当线程数超过/proc/sys/kernel/threads-max中规定的上限时产生OOM崩溃。
for循环执行测试代码:
private void sendData(final int num) {
Observable.create(new Observable.OnSubscribe<Integer>() {
@Override public void call(Subscriber<? super Integer> subscriber) {
try {
Thread.sleep(400);
} catch (InterruptedException e) {
e.printStackTrace();
}
subscriber.onNext(num);
subscriber.onCompleted();
}
}).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()).subscribe(new Observer<Integer>() {
@Override public void onNext(Integer num) {
Log.d(TAG, "current_tread == " + Thread.currentThread().getId());
}
@Override public void onCompleted() {
Log.d(TAG, "work_num == " + num);
}
@Override public void onError(Throwable e) {
}
});
}
- 结论:当for循环超过600时,得到和上报日志完全相同的Crash,IO调度器的线程池失效了。为什么IO调度器的线程池会失效呢?
2.4 定位与解决
我们看Scheduler.io
的原码发现,里面的线程池是一个可以自增、无上限的线程池,而且每个线程设置了一个60s的保活时间防止被结束。也就是说:在60s内做频繁操作时,io调度器线程池并没有约束线程数且会不断开新线程。我们搜索代码查到Dig打点库中上传数据时,有频繁密集请求网络,并使用IO了调度器,导致不断开启线程最终crash。
解决办法:在这种密集频繁的操作时,自己指定一个Executor作为调度器。
2.5 监控措施
可以利用linux的inotify机制进行监控:
- watch /proc/pid/task来监控线程使用情况。