Crash排查系列第二篇|一种native线程数触顶问题排查方式

643 阅读4分钟

遇到问题

java.lang.OutOfMemoryError: pthread_create(1040KB stack) failed: Out of Memory image.png

查看系统抛出异常过程

art/runtime/thread.cc

Native Thread::CreateNativeThread中抛出的OOM ,通过message信息结合代码上下文 很容易判断出是 pthread_create过程中返回了错误码。

pthread_create第一处会返回错误码的地方,分配stacksize内存的时候 这种大概率是虚拟内存不足,不过返回的是EAGAIN 也就是try again,与message不匹配。

第二处返回错误码的地方 就是clone方法返回的 。并且会打印clone failed日志。 结合logcat 确认是这个问题。 这种情况大概率可以归结为线程数问题。

查看线程信息

  1. /proc/{pid}/status 信息上报 VmSize: 7405776 kB VmRSS: 429792 kB Threads: 500

线程数确实到了500,在 Android7.0 及以上的华为手机(EmotionUI_5.0 及以上)的手机产生 OOM,这些手机的线程数限制都很小 ,每个进程只允许最大同时开 500 个线程。

  1. 查看java 线程数及堆栈 Thread.getAllStackTraces().size() thread num=182

结合以上线程信息,我们可以判定是native线程有问题了,接下来就是如何去定位native 线程数导致crash问题了。

系统触发threaddump的原理

我们知道在发生anr时。系统会dump出很详细的线程状态信息,kernel,native,java堆栈信息,探究一下其中的原理。

backtrace dump方式简单说明就是通过/proc/{pid}/task 遍历获取tid 然后往线程发kThreadUnwindSignal 信号,在SignalHandler中获取到该线程当前寄存器信息 再进行回溯。如果我们可以直接调用art::ThreadList::DumpForSigQuit是不是就能获取到线程信息了

主动触发backtrace

找到thread_list_偏移地址

  1. 有一种办法是完整的把runtime.h 按顺序构建一个struct 通过下面的方式获取到thread_list_。

不过这种方式有个缺陷,那就是每个不同系统版本都有可能会有字段添加删除导致最终offset出问题,这样我们就要对每个版本都创建一个对应的struct 维护对应的顺序。

Runtime_7X* runtime7X = (Runtime_7X*)runtime_instance_;
return runtime7X->thread_list_;
struct Runtime_7X {

    uint64_t callee_save_methods_[3];
    void* pre_allocated_OutOfMemoryError_;
    void* pre_allocated_NoClassDefFoundError_;
    void* resolution_method_;
    void* imt_conflict_method_;
    // Unresolved method has the same behavior as the conflict method, it is used by the class linker
    // for differentiating between unfilled imt slots vs conflict slots in superclasses.
    void* imt_unimplemented_method_;
    void* sentinel_;

    int instruction_set_;
    uint32_t callee_save_method_frame_infos_[9]; // QuickMethodFrameInfo = uint32_t * 3

    void* compiler_callbacks_;
    bool is_zygote_;
    bool must_relocate_;
    bool is_concurrent_gc_enabled_;
    bool is_explicit_gc_disabled_;
    bool dex2oat_enabled_;
    bool image_dex2oat_enabled_;

    std::string compiler_executable_;
    std::string patchoat_executable_;
    std::vector<std::string> compiler_options_;
    std::vector<std::string> image_compiler_options_;
    std::string image_location_;

    std::string boot_class_path_string_;
    std::string class_path_string_;
    std::vector<std::string> properties_;

    // The default stack size for managed threads created by the runtime.
    size_t default_stack_size_;

    void* heap_;
    void* jit_arena_pool_;
    void* arena_pool_;
    void* low_4gb_arena_pool_;

    // Shared linear alloc for now.
    void* linear_alloc_;

    // The number of spins that are done before thread suspension is used to forcibly inflate.
    size_t max_spins_before_thin_lock_inflation_;
    void* monitor_list_;
    void* monitor_pool_;

    void* thread_list_;
}; 
  1. 第二种方案,这种方案相当于是对第一种方案的完善,通过减少字段改动的方式减少出错的概率。 在thread_list_后面我们发现还有个java_vm_字段,这个字段我们是可以通过env->GetJavaVM拿到的,那就有了个方案 我们先可以找到runtime中java_vm_的offset 再反过来找到thread_list_。这样中间涉及的字段就大大减少了,也减小了出错的概率。

image.png

image.png

image.png

image.png

找到art::ThreadList::DumpForSigQuit方法地址

art::ThreadList::DumpForSigQuit(std::__1::basic_ostream<char, std::__1::char_traits >&)

image.png

Dump backtrace的crash保护

这边有一些稳定性问题,比如你mock的struct顺序不对就可能会抛出sigsegv,

image.png

我们可以利用sigsetjmp/siglongjmp的机制做一个try catch。github.com/bytedance/a… 保护后我们就不会crash

image.png 但是这样又带来一个新的问题anr,排查原因我们发现是list_部分crash了,而前面又加了个锁。 导致锁没有释放,其他线程就无法获取到这个锁。 我这边的解决方式是提前调用一个其他使用到list_但没有加锁的方法来判断thread_list_是否有效 比如FindThreadByThreadId

image.png

image.png

image.png

触发backtrace dump时机

为了把影响面降低到最少,我们选择在相应线程crash的时候做dump,crash的时候我们先过滤条件,比如华为设备线程数超过400个的crash 我们就进行dump

获取线程数

获取所有线程数
public static int getThreadNum() {
String sb = "/proc/" + Process.myPid() + "/task";
final File[] listFiles = new File(sb).listFiles();
if (listFiles != null) {
    return listFiles.length;
}
return 0;
}

获取java线程数

Thread.activeCount()

问题定位

低端机播放器实例过多占用了几百个线程。