专栏首页逮虾户Thread也会OOM吗?

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 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();
            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(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之类的,我们自己定一个一个大的蓄水池,然后把线程池的总数给定义死,之后我们去替换项目内的所有用到线程池的地方。

想想就有点小激动,先想想怎么做,再来决定方法论。

  1. 定义好不需要替换的白名单
  2. 遍历查找所有的类,寻找到线程池的构造函数。
  3. 把构造函数替换成我们的共享线程池。

又是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 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

总结

其实这个方案之前也想了一段时间了,最近要离职了,才抽出时间去写去优化,也算对asm加深了一些理解和使用吧。

其实我看猫眼的官博前几天已经发表过类似的文章了,但是我觉得其实还是有些别的可以改进的,虽然这个问题18年就有人写了,但是我觉得努努力还是可以突破下边界的。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • jvm堆内存溢出后,其他线程是否可继续工作

    最近网上出现一个美团面试题:“一个线程OOM后,其他线程还能运行吗?”。我看网上出现了很多不靠谱的答案。这道题其实很有难度,涉及的知识点有jvm内存分配、作用域...

    美的让人心动
  • jvm堆内存溢出后,其他线程是否可继续工作

    最近网上出现一个美团面试题:“一个线程OOM后,其他线程还能运行吗?”。我看网上出现了很多不靠谱的答案。这道题其实很有难度,涉及的知识点有jvm内存分配、作用域...

    纯洁的微笑
  • 美团面试题:JVM堆内存溢出后,其他线程是否可继续工作?

    最近网上出现一个美团面试题:“一个线程OOM后,其他线程还能运行吗?”。我看网上出现了很多不靠谱的答案。这道题其实很有难度,涉及的知识点有jvm内存分配、作用域...

    Spark学习技巧
  • 某团面试题:JVM 堆内存溢出后,其他线程是否可继续工作?

    最近网上出现一个美团面试题:“一个线程OOM后,其他线程还能运行吗?”。我看网上出现了很多不靠谱的答案。这道题其实很有难度,涉及的知识点有jvm内存分配、作用域...

    芋道源码
  • 某团面试题:JVM 堆内存溢出后,其他线程是否可继续工作?

    最近网上出现一个美团面试题:“一个线程OOM后,其他线程还能运行吗?”。我看网上出现了很多不靠谱的答案。这道题其实很有难度,涉及的知识点有jvm内存分配、作用域...

    乔戈里
  • 某团面试题:JVM 堆内存溢出后,其他线程是否可继续工作?

    这道题其实很有难度,涉及的知识点有jvm内存分配、作用域、gc等,不是简单的是与否的问题。

    良月柒
  • 美团面试:JVM 堆内存溢出后,其他线程是否可继续工作?

    我看网上出现了很多不靠谱的答案。这道题其实很有难度,涉及的知识点有jvm内存分配、作用域、gc等,不是简单的是与否的问题。

    程序猿DD
  • 某团面试题:JVM 堆内存溢出后,其他线程是否可继续工作?

    这道题其实很有难度,涉及的知识点有jvm内存分配、作用域、gc等,不是简单的是与否的问题。

    公众号 IT老哥
  • 某团面试题:JVM 堆内存溢出后,其他线程是否可继续工作?

    我看网上出现了很多不靠谱的答案。这道题其实很有难度,涉及的知识点有jvm内存分配、作用域、gc等,不是简单的是与否的问题。

    java进阶架构师
  • JVM 堆内存溢出后,其他线程是否可继续工作?

    我看网上出现了很多不靠谱的答案。这道题其实很有难度,涉及的知识点有jvm内存分配、作用域、gc等,不是简单的是与否的问题。

    用户2781897
  • 面试官欺负人:一个线程 OOM 后,其他线程还能运行吗?

    我看网上出现了很多不靠谱的答案。这道题其实很有难度,涉及的知识点有jvm内存分配、作用域、gc等,不是简单的是与否的问题。

    Java技术栈
  • 【Java】一次 OOM 试验造成的电脑雪崩引发的思考

    在写「垃圾回收-实战篇」时,按书中的一个例子做了一次实验,我觉得涉及的知识点挺多的,所以单独拎出来与大家共享一下,相信大家看完肯定有收获。

    Rude3Knife的公众号
  • 刨根问底---一次 OOM 试验造成的电脑雪崩引发的思考

    在写「垃圾回收-实战篇」时,按书中的一个例子做了一次实验,我觉得涉及的知识点挺多的,所以单独拎出来与大家共享一下,相信大家看完肯定有收获。

    kunge
  • Tomcat 的线程池实现原理

    ThreadPoolExecutor回收不了,可以看看其源码,工作线程Worker是内部类,只要它活着,换句话说线程在跑,就会阻止ThreadPoolExecu...

    JavaEdge
  • 《Android 创建线程源码与OOM分析》

    | 导语 企鹅FM近几个版本的外网Crash出现很多OutOfMemory(以下简称OOM)问题,Crash的堆栈都在Thread::start方法上。该文详细...

    腾讯Bugly
  • 我是一个线程池

    我是一个线程池(ThreadPoolExecutor),我的主要工作是管理在我这的多个线程(Thread),让他们能并发地执行多个任务的同时,又不会造成很大的的...

    kunge
  • 我是一个线程池(细节修订版)

    你好呀,我是沉默王二,不不不,我是一个线程池(ThreadPoolExecutor),我的主要工作是管理在我这个池子里的多个线程(Thread),让他们能并发地...

    沉默王二
  • 记一次OOM问题排查过程

    看线程名称应该是tomcat的nio工作线程,线程在处理程序的时候因为无法在堆中分配更多内存出现了OOM,幸好JVM启动参数配置了-XX:+HeapDumpOn...

    田维常
  • MySQL数据查询太多会OOM吗?

    我的主机内存只有100G,现在要全表扫描一个200G大表,会不会把DB主机的内存用光?

    JavaEdge

扫码关注云+社区

领取腾讯云代金券