遇到问题
java.lang.OutOfMemoryError: pthread_create(1040KB stack) failed: Out of Memory
查看系统抛出异常过程
art/runtime/thread.cc
Native Thread::CreateNativeThread中抛出的OOM ,通过message信息结合代码上下文 很容易判断出是 pthread_create过程中返回了错误码。
pthread_create第一处会返回错误码的地方,分配stacksize内存的时候 这种大概率是虚拟内存不足,不过返回的是EAGAIN 也就是try again,与message不匹配。
第二处返回错误码的地方 就是clone方法返回的 。并且会打印clone failed日志。 结合logcat 确认是这个问题。 这种情况大概率可以归结为线程数问题。
查看线程信息
- /proc/{pid}/status 信息上报 VmSize: 7405776 kB VmRSS: 429792 kB Threads: 500
线程数确实到了500,在 Android7.0 及以上的华为手机(EmotionUI_5.0 及以上)的手机产生 OOM,这些手机的线程数限制都很小 ,每个进程只允许最大同时开 500 个线程。
- 查看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_偏移地址
- 有一种办法是完整的把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_;
};
- 第二种方案,这种方案相当于是对第一种方案的完善,通过减少字段改动的方式减少出错的概率。 在thread_list_后面我们发现还有个java_vm_字段,这个字段我们是可以通过env->GetJavaVM拿到的,那就有了个方案 我们先可以找到runtime中java_vm_的offset 再反过来找到thread_list_。这样中间涉及的字段就大大减少了,也减小了出错的概率。
找到art::ThreadList::DumpForSigQuit方法地址
art::ThreadList::DumpForSigQuit(std::__1::basic_ostream<char, std::__1::char_traits >&)
Dump backtrace的crash保护
这边有一些稳定性问题,比如你mock的struct顺序不对就可能会抛出sigsegv,
我们可以利用sigsetjmp/siglongjmp的机制做一个try catch。github.com/bytedance/a… 保护后我们就不会crash
但是这样又带来一个新的问题anr,排查原因我们发现是list_部分crash了,而前面又加了个锁。 导致锁没有释放,其他线程就无法获取到这个锁。 我这边的解决方式是提前调用一个其他使用到list_但没有加锁的方法来判断thread_list_是否有效 比如FindThreadByThreadId
触发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()
问题定位
低端机播放器实例过多占用了几百个线程。