创建线程OOM问题分析与解决

前言

系统的学习、总结. 千万不要再犯零零散散学习的毛病了

一、线程OOM

  • 1、背景
  • 2、Android内存管理策略
  • 3、源码分析
  • 4、原因猜测
  • 5、线程数据获取
  • 6、线程执行流程监控

1.1 OOM背景

1.1.1 错误一
java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Out of memory
java.lang.Thread.nativeCreate(Native Method)
java.lang.Thread.start(Thread.java:745)
...
1.1.2 错误二
java.lang.OutOfMemoryError: Could not allocate JNI Env
java.lang.Thread.nativeCreate(Native Method)
java.lang.Thread.start(Thread.java:729)
...

1.2 Android内存管理策略

  OOM并不等于RAM不足, 这和Android的内存管理策略有关.
  内存分为虚拟内存和物理内存. 通过malloc和new分配的内存都是虚拟地址空间的内存. 虚拟地址空间比物理内存要大, 在较多进程同时运行时, 物理地址空间有可能不够.
  针对这种情况Linux采用的"进程内存最大化"的分配策略, 用Swap机制来保证物理内存不被消耗尽, 把最近最少使用的空间腾到外部存储空间上, 假装还是存储在RAM里.
  虽然Android基于Linux, 但是在内存策略上有自己的模式—没有交换区.
  Android进程分配策略是每个进程都有一个内存占用限制, 这个具体大小由手机具体配置决定. 目的就是为了让更多的进程都保留在RAM中, 这样每个进程被唤起的时候可以避免外部存储到内部存储的数据读写的消耗, 加快更多的App恢复的响应速度, 也避免了流氓App抢占所有内存. Android采用自己的LowMemoryKill策略来控制RAM中的进程. 如果RAM真的不足, MemoryKiller就会杀死一些优先级比较低的进程来释放物理内存.

1.3 Thread.start源码分析

在这里插入图片描述
Linux相关知识还需要继续学习, 由于Linux相关知识薄弱, 看源码理解Thread.start流程还是挺不容易的

1. thread.CreateNativeThread
void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon) {
	Thread* self = static_cast<JNIEnvExt*>(env)->self;
	Runtime* runtime = Runtime::Current();
  	Thread* child_thread = new Thread(is_daemon);
  	// Use global JNI ref to hold peer live while child thread starts.
  	child_thread->tlsPtr_.jpeer = env->NewGlobalRef(java_peer);
  	stack_size = FixStackSize(stack_size);
  	std::string error_msg;
  	// 1.创建结构体JNIEnvExt
  	std::unique_ptr<JNIEnvExt> child_jni_env_ext(
    	  JNIEnvExt::Create(child_thread, Runtime::Current()->GetJavaVM(), &error_msg));
  	int pthread_create_result = 0;
  	if (child_jni_env_ext.get() != nullptr) {
    	pthread_t new_pthread;
    	pthread_attr_t attr;
    	child_thread->tlsPtr_.tmp_jni_env = child_jni_env_ext.get();
    	// 2.创建线程
    	pthread_create_result = pthread_create(&new_pthread,
                                           	   &attr,
                                           	   Thread::CreateCallback,
                                           	   child_thread);
    }
  	// TODO: remove from thread group?
  	env->SetLongField(java_peer, WellKnownClasses::java_lang_Thread_nativePeer, 0);
  	{
  		// 3.如果结构体child_jni_env_ext创建失败, 抛出异常一(Cloud not allocate JNI Env), 反之抛出异常二
    	std::string msg(child_jni_env_ext.get() == nullptr ?
        	StringPrintf("Could not allocate JNI Env: %s", error_msg.c_str()) :
        	StringPrintf("pthread_create (%s stack) failed: %s",
                                PrettySize(stack_size).c_str(), strerror(pthread_create_result)));
    	ScopedObjectAccess soa(env);
    	soa.Self()->ThrowOutOfMemoryError(msg.c_str());
  	}
}

主要做了三件事:

  • 1、创建结构体JNIEnvExt
  • 2、如果结构体JNIEnvExt创建成功, 创建线程
  • 3、判断线程是否创建成功, 如果失败, 抛出OOM异常, 并设置对应错误信息
2. pthread_create.pthread_create

线上报的OOM: pthread_create failed异常, 所以关注第二个流程pthread_create流程

int pthread_create(pthread_t* thread_out, pthread_attr_t const* attr,
                   void* (*start_routine)(void*), void* arg) {
  	ErrnoRestorer errno_restorer;
  	// Inform the rest of the C library that at least one thread was created.
  	__isthreaded = 1;
  	pthread_attr_t thread_attr;
  	pthread_internal_t* thread = NULL;
  	void* child_stack = NULL;
  	// 创建线程
  	int result = __allocate_thread(&thread_attr, &thread, &child_stack);
  	if (result != 0) {
    	return result;
	}
}

中间的调用流程省略直接关注__allocate_thread最后核心逻辑

3. pthread_create.__create_thread_mapped_space
static void* __create_thread_mapped_space(size_t mmap_size, size_t stack_guard_size) {
  	// Create a new private anonymous map.
  	int prot = PROT_READ | PROT_WRITE;
  	int flags = MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE;
  	void* space = mmap(NULL, mmap_size, prot, flags, -1, 0);
  	if (space == MAP_FAILED) {
    	return NULL;
	}
	return space;
}

这段逻辑是调用Linux系统函数mmap创建虚拟内存. 如果创建虚拟内存失败, 返回NULL, 执行到 thread.CreateNaticeThread 中的第二个异常, 抛出pthread_create.

1.4 可能原因

  • 1、线程数超限: 即proc/pid/status中记录的线程数量突破/proc/sys/kernel/threads-max中规定的最大线程数

  • 2、java堆内存超限: 申请的堆内存大小超过了Runtime.getRuntime().maxMemory()

二、OOM问题解决

  • 1、获取进程线程数量
  • 2、获取系统为进程分配的线程最大数量

2.1 获取线程相关数量

2.1.1 命令行获取
// 1. 获取进程pid
adb shell ps 
// 2. 获取指定进程线程数量(Threads选项)
adb shell
cat /proc/pid/status
// 3. 查看指定进程中线程数量
adb shell ps -T <pid> | wc -l
// 4. 查看进程中线程tid
adb shell ps -T -p <pid>
// 5. 查看线程总数量
adb shell 
cat /proc/sys/kernel/threads-max
// 6. 查看线程变化
watch -n 1 -d 'adb shell ps -T | grep XXX | wc -l'
2.1.2 代码获取线程数量
1. 获取线程数量
private void getThreadCount() throws Exception {
	int i = android.os.Process.myPid();
    java.lang.Process p = Runtime.getRuntime().exec("cat /proc/" + i + "/status");
    InputStream is = p.getInputStream();
    InputStreamReader isr = new InputStreamReader(is);
    BufferedReader br = new BufferedReader(isr);
    String line;
    while ((line = br.readLine()) != null) {
        if (line.startsWith("Threads:")) {
            Log.v("AndroidTest", "line = " + line);
        }
    }
}

//输出
com.test.apm V/AndroidTest: line = Threads:	15
2. 获取线程具体信息
private void getThreadInfo() {
    ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
    while ((threadGroup.getParent()) != null) {
        // 返回此线程组的父线程组
        threadGroup = threadGroup.getParent();
    }
    Thread[] threads = new Thread[threadGroup.activeCount()];
    Log.v("AndroidTest", "threadCount = " + threadGroup.activeCount());
    // 把对此线程组中的所有活动子组的引用复制到指定数组中
    threadGroup.enumerate(threads);
    for (int i = 0; i < threads.length; i++) {
        Log.v("AndroidTest", "thread[" + i + "] = " + threads[i].getName());
    }
}
//输出
thread[0] = Signal Catcher
thread[1] = ReferenceQueueDaemon
thread[2] = FinalizerDaemon
thread[3] = FinalizerWatchdogDaemon
thread[4] = HeapTaskDaemon
thread[5] = Profile Saver
thread[6] = process reaper
thread[7] = main
thread[8] = Jit thread pool worker thread 0
thread[9] = Binder:7779_1
thread[10] = Binder:7779_2
thread[11] = Binder:7779_3
thread[12] = RenderThread
thread[13] = queued-work-looper

三、线程创建流程的监控

  • 1、ASM字节码修改
  • 2、具体使用booster
  • 3、hook pthread_create

3.1 ASM字节码修改

找到创建线程的地方, 然后进行编译时修改

  • 1、Thread及其子类
  • 2、ThreadPoolExecutor及其子类、Executors、ThreadFactor实现类
  • 3、AsyncTask
  • 4、Timer及其子类

3.2 booster

利用ASM对字节码修改, 将所有创建线程的指令在编译期间替换成自定义的方法调用, 为线程名加上调用者的类名前缀, 实现追踪线程创建来源.

3.3 hook pthread_create

结合线程start的流程, 可知线程创建最终会走到pthread_create处, hook该函数即可. 这个已经有现成的框架 epic, 直接使用即可完成线程创建以及run的监听.

总结

关于线程OOM, 使用稍有不当, 便会出现该问题, 包括OkHttpClient、自定义线程池、RxJava的使用不当, 都会造成OOM, 一般也就是这几个地方.
实际解决思路是:

  • 1、测试环境加定时器, 定时获取线程数量, 判断是否超过自定义阈值
  • 2、如果超过自定义阈值, 通过ThreadGroup获取每个线程的信息
  • 3、使用epic, 线程创建时map缓存[threadId, stackInfo], 执行完run之后, 清除掉该缓存, 当线程数量超过阈值时, 上报一次该map信息
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值