华为手机6.0线程OOM分析

作者: 首席咸鱼腌制师 | 来源:发表于2018-05-13 16:14 被阅读0次

    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来监控线程使用情况。

    相关文章

      网友评论

        本文标题:华为手机6.0线程OOM分析

        本文链接:https://www.haomeiwen.com/subject/ppuldftx.html