本文首发掘金:Thread也会OOM吗?
作者:究极逮虾户
OOM其实是一个比较常见的异常了,但是不知道各位老哥有没有见过这个异常。
java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Try again
at java.lang.Thread.nativeCreate(Thread.java)
at java.lang.Thread.start(Thread.java:1076)
at java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:920)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1338)
...
由于国内手机厂商的奇奇怪怪的优化,特别是华为,其对于线程的构建有特别严苛的要求,当进程内总线程数量达到一定的量级的情况下就会发生线程OOM问题。
这个问题其实有人专门做过分析,我这个人还是不喜欢直接复制别人的文章,但是读书人嘛,借书怎么能叫偷呢。不可思议的OOM
在Android7.0及以上的华为手机(EmotionUI_5.0及以上)的手机产生OOM,这些手机的线程数限制都很小(应该是华为rom特意修改的limits),每个进程只允许最大同时开500个线程,因此很容易复现了。
for (i in 0 until 3000) {
Thread {
while (true) {
Thread.sleep(1000)
}
}.start()
}
这个是作者做的一个实验,当华为手机的线程创建超过500的时候就会发生崩溃的问题了。但是我自己写了个demo,发现也不是所有的华为手机都这样,我用NOVA7测试出来的结果大概是3000个线程才会出现崩溃的问题。
线上真的会有超过500个线程的情况出现吗?
如何查看当前线程数量?
Android Profiler 工具非常强大,里面就有当前进程启动的线程数量,以及其cpu调度情况的。

图上可以看出来THREADS
后面的就是当前的线程使用数量。一个只含有少量代码的安卓项目执行的时候其实也有大概30条左右的线程存在,而OKHttp,Glide,第三方框架,Socket以及启动任务栈等等第三方框架接入后,线程数量更是会出现一个井喷式增长。
线上问题原因分析?
我观察了下我们的项目的线程使用情况,发现当项目完成简单的初始化之后就会构建出大概300条左右的线程,其实还是比较感人的。而线上的使用情况很复杂,而且报错日志上的错误并不是oom的真实原因,而是压死骆驼的最后一根稻草。
我其实在上家公司的时候就发生过这个问题,当时我们跟踪源代码,发现在使用rxjava的Schedulers.io()
导致的这个问题。
static final class CachedWorkerPool implements Runnable {
private final long keepAliveTime;
private final ConcurrentLinkedQueue<ThreadWorker> expiringWorkerQueue;
final CompositeDisposable allWorkers;
private final ScheduledExecutorService evictorService;
private final Future<?> evictorTask;
private final ThreadFactory threadFactory;
CachedWorkerPool(long keepAliveTime, TimeUnit unit, ThreadFactory threadFactory) {
this.keepAliveTime = unit != null ? unit.toNanos(keepAliveTime) : 0L;
this.expiringWorkerQueue = new ConcurrentLinkedQueue<ThreadWorker>();
this.allWorkers = new CompositeDisposable();
this.threadFactory = threadFactory;
ScheduledExecutorService evictor = null;
Future<?> task = null;
if (unit != null) {
evictor = Executors.newScheduledThreadPool(1, EVICTOR_THREAD_FACTORY);
task = evictor.scheduleWithFixedDelay(this, this.keepAliveTime, this.keepAliveTime, TimeUnit.NANOSECONDS);
}
evictorService = evictor;
evictorTask = task;
}
@Override
public void run() {
evictExpiredWorkers();
}
ThreadWorker get() {
if (allWorkers.isDisposed()) {
return SHUTDOWN_THREAD_WORKER;
}
while (!expiringWorkerQueue.isEmpty()) {
ThreadWorker threadWorker = expiringWorkerQueue.poll();
if (threadWorker != null) {
return threadWorker;
}
}
// No cached worker found, so create a new one.
ThreadWorker w = new ThreadWorker(threadFactory);
allWorkers.add(w);
return w;
}
void release(ThreadWorker threadWorker) {
// Refresh expire time before putting worker back in pool
threadWorker.setExpirationTime(now() + keepAliveTime);
expiringWorkerQueue.offer(threadWorker);
}
void evictExpiredWorkers() {
if (!expiringWorkerQueue.isEmpty()) {
long currentTimestamp = now();
for (ThreadWorker threadWorker : expiringWorkerQueue) {
if (threadWorker.getExpirationTime() <= currentTimestamp) {
if (expiringWorkerQueue.remove(threadWorker)) {
allWorkers.remove(threadWorker);
}
} else {
// Queue is ordered with the worker that will expire first in the beginning, so when we
// find a non-expired worker we can stop evicting.
break;
}
}
}
}
long now() {
return System.nanoTime();
}
void shutdown() {
allWorkers.dispose();
if (evictorTask != null) {
evictorTask.cancel(true);
}
if (evictorService != null) {
evictorService.shutdownNow();
}
}
}
public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue(), threadFactory);
}
从上述代码可以分析出,IO实现其实就是一个线程池,其核心数为1,最大线程数为Integer.MAX_VALUE
,然后线程的销毁时间为60s。这个其实很多文章都有介绍的,也算是一个常规的改点,我们把这个线程池替换了之后的确是对项目线程OOM问题有所下降。
RxJavaPlugins.setInitIoSchedulerHandler {
val processors = Runtime.getRuntime().availableProcessors()
val executor = ThreadPoolExecutor(processors * 2,
processors * 10, 1, TimeUnit.SECONDS, LinkedBlockingQueue<Runnable>(processors*10),
ThreadPoolExecutor.DiscardPolicy()
)
Schedulers.from(executor)
}
小贴士 这边需要注意一定要在第一次调用rxjava之前执行RxJavaPlugins,否则代码会失效。
Kotlin的协程的IO线程实现机制上也是线程池。之前的文章介绍过,协程的内部的线程调度器的实现其实和rxjava的是一样的,都是一个线程池。我仔细观察了下DefaultScheduler.IO
的实现。
open class ExperimentalCoroutineDispatcher(
private val corePoolSize: Int,
private val maxPoolSize: Int,
private val idleWorkerKeepAliveNs: Long,
private val schedulerName: String = "CoroutineScheduler"
) : ExecutorCoroutineDispatcher() {
constructor(
corePoolSize: Int = CORE_POOL_SIZE,
maxPoolSize: Int = MAX_POOL_SIZE,
schedulerName: String = DEFAULT_SCHEDULER_NAME
) : this(corePoolSize, maxPoolSize, IDLE_WORKER_KEEP_ALIVE_NS, schedulerName)
@JvmField
internal val IDLE_WORKER_KEEP_ALIVE_NS = TimeUnit.SECONDS.toNanos(
systemProp("kotlinx.coroutines.scheduler.keep.alive.sec", 60L)
)
其中线程存活时间为60s,最大线程数则是根据系统配置获取的,我查阅了下stackoverflow发现了这个值的大小为64。那么协程的IO调用的其实也还好,并不会导致线程OOM问题。而且这个值其实也可以由开发去修正,也还是可以限制的。
接下来又可以表现真正的技术了
如果你以为我只有上面这么一点点水平,那么我肯定不会写这篇文章吹牛皮了。
以上只能解决当前项目上可以被修改的一些线程池相关的,那么有没有办法直接修改第三方的线程池构建呢????比如第三方聊天,阿里的一些库等等。
如果我们可以把当前项目内,除了OkHttp,Glide之类的,我们自己定一个一个大的蓄水池,然后把线程池的总数给定义死,之后我们去替换项目内的所有用到线程池的地方。
想想就有点小激动,先想想怎么做,再来决定方法论。
- 定义好不需要替换的白名单
- 遍历查找所有的类,寻找到线程池的构造函数。
- 把构造函数替换成我们的共享线程池。
又是transfrom,为什么老是我
首先我在原先的双击优化的demo上增加了一个小小的功能,就是上面我罗列的那些,通过类查找,然后替换的方式完成线程池构造的替换操作。
public class ThreadPoolMethodVisitor extends MethodVisitor {
public ThreadPoolMethodVisitor(MethodVisitor mv) {
super(Opcodes.ASM5, mv);
}
@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
boolean isThreadPool = isThreadPool(opcode, owner, name, desc);
if (isThreadPool) {
JLog.info("owner:" + owner + " name:" + name + " desc:" + desc);
mv.visitInsn(Opcodes.POP);
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "com/wallstreetcn/sample/utils/TestIOThreadExecutor",
"getTHREAD_POOL_SHARE",
"()Lcom/wallstreetcn/sample/utils/TestIOThreadExecutor;", itf);
} else {
super.visitMethodInsn(opcode, owner, name, desc, itf);
}
}
@Override
public void visitInsn(int opcode) {
super.visitInsn(opcode);
}
@Override
public void visitLineNumber(int line, Label start) {
super.visitLineNumber(line, start);
}
boolean isThreadPool(int opcode, String owner, String name, String desc) {
List<PoolEntity> list = ThreadPoolCreator.INSTANCE.getPoolList();
for (PoolEntity poolEntity : list) {
if (opcode != poolEntity.getCode()) {
continue;
}
if (!owner.equals(poolEntity.getOwner())) {
continue;
}
if (!name.equals(poolEntity.getName())) {
continue;
}
if (!desc.equals(poolEntity.getDesc())) {
continue;
}
return true;
}
return false;
}
}
上面是一个MethodVisitor,任意的一个方法块都会被这个类访问到,然后我们可以根据访问信息,以及方法名,类名等关键信息,对这个方法块进行修改。
我这里生成了一个列表,我会把所有关于线程池构造的实体都放到这个列表中,然后把当前的方法调用拿去其中匹配,当发现是一个线程池的构造函数的时候,我们就对代码进行修改插入,替换成我们的共享线程池。这样我们就能对在编译环节对线程池构造进行替换,约束项目的所有线程池的构建。
除了这个呢?
其实还能通过一部分静态扫描的形势去约束开发人员,你不允许直接new线程的方式去创建一个线程,这样也能对这部分OOM的治理,自己写个lint就行了。
补充下 lint 的demo我也写好了,各位有时间就看看,没时间也就算了https://github.com/Leifzhang/AndroidLint
文末
感谢大家关注我,分享Android干货,交流Android技术。
对文章有何见解,或者有何技术问题,都可以在评论区一起留言讨论,我会虔诚为你解答。
也欢迎大家来我的B站找我玩,有各类Android架构师进阶技术难点的视频讲解,助你早日升职加薪。
B站直通车:https://space.bilibili.com/544650554![]()
网友评论