原文链接:
https://juejin.im/post/5d6a57eee51d4561e721df30#heading-0
前言
(想自学习编程的小伙伴请搜索圈T社区,更多行业相关资讯更有行业相关免费视频教程。完全免费哦!)
内存飙升问题复现
实例代码
ExecutorService executor = Executors.newFixedThreadPool(10); for (int i = 0; i < Integer.MAX_VALUE; i++) { executor.execute(() -> { try { Thread.sleep(10000); } catch (InterruptedException e) { //do nothing } }); }
配置Jvm参数
IDE指定JVM参数:-Xmx8m -Xms8m :
执行结果
run以上代码,会抛出OOM:
JVM OOM问题一般是创建太多对象,同时GC 垃圾来不及回收导致的,那么什么原因导致线程池的OOM呢?带着发现新大陆的心情,我们从源码角度分析这个问题,去找找实例代码中哪里创了太多对象。
线程池源码分析
以上的实例代码,就一个newFixedThreadPool和一个execute方法。首先,我们先来看一下newFixedThreadPool方法的源码
newFixedThreadPool源码
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
该段源码以及结合线程池特点,我们可以知道newFixedThreadPool:
线程池特点了解不是很清楚的朋友,可以看我这篇文章,面试必备:Java线程池解析
接下来,我们再来看看线程池执行方法execute的源码。
线程池执行方法execute的源码
execute的源码以及相关解释如下:
public void execute(Runnable command) { if (command == null) throw new NullPointerException(); int c = ctl.get(); if (workerCountOf(c) < corePoolSize) { //步骤一:判断当前正在工作的线程是否比核心线程数量小 if (addWorker(command, true)) // 以核心线程的身份,添加到工作集合 return; c = ctl.get(); } //步骤二:不满足步骤一,线程池还在RUNNING状态,阻塞队列也没满的情况下,把执行任务添加到阻塞队列workQueue。 if (isRunning(c) && workQueue.offer(command)) { int recheck = ctl.get(); //来个double check ,检查线程池是否突然被关闭 if (! isRunning(recheck) && remove(command)) reject(command); else if (workerCountOf(recheck) == 0) addWorker(null, false); } //步骤三:如果阻塞队列也满了,执行任务以非核心线程的身份,添加到工作集合 else if (!addWorker(command, false)) reject(command); }
纵观以上代码,我们可以发现就addWorker 以及workQueue.offer(command) 可能在创建对象。那我们先分析addWorker方法。
addWorker源码分析
addWorker源码以及相关解释如下
private boolean addWorker(Runnable firstTask, boolean core) { retry: for (;;) { int c = ctl.get(); //获取当前线程池的状态 int rs = runStateOf(c); //如果线程池状态是STOP,TIDYING,TERMINATED状态的话,则会返回false。 // 如果现在状态是SHUTDOWN,但是firstTask不为空或者workQueue为空的话,那么直接返回false if (rs >= SHUTDOWN && ! (rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty())) return false; //自旋 for (;;) { //获取当前工作线程的数量 int wc = workerCountOf(c); //判断线程数量是否符合要求,如果要创建的是核心工作线程,判断当前工作线程数量是否已经超过coreSize, // 如果要创建的是非核心线程,判断当前工作线程数量是否超过maximumPoolSize,是的话就返回false if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize)) return false; //如果线程数量符合要求,就通过CAS算法,将WorkerCount加1,成功就跳出retry自旋 if (compareAndIncrementWorkerCount(c)) break retry; c = ctl.get(); // Re-read ctl if (runStateOf(c) != rs) continue retry; retry inner loop } } //线程启动标志 boolean workerStarted = false; //线程添加进集合workers标志 boolean workerAdded = false; Worker w = null; try { //由(Runnable 构造Worker对象 w = new Worker(firstTask); final Thread t = w.thread; if (t != null) { //获取线程池的重入锁 final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { //获取线程池状态 int rs = runStateOf(ctl.get()); //如果状态满足,将Worker对象添加到workers集合 if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) { if (t.isAlive()) throw new IllegalThreadStateException(); workers.add(w); int s = workers.size(); if (s > largestPoolSize) largestPoolSize = s; workerAdded = true; } } finally { mainLock.unlock(); } //启动Worker中的线程开始执行任务 if (workerAdded) { t.start(); workerStarted = true; } } } finally { //线程启动失败,执行addWorkerFailed方法 if (! workerStarted) addWorkerFailed(w); } return workerStarted; }
addWorker执行流程
大概就是判断线程池状态是否OK,如果OK,在判断当前工作中的线程数量是否满足(小于coreSize/maximumPoolSize),如果不满足,不添加,如果满足,就将执行任务添加到工作集合workers,,并启动执行该线程。
再看一下workers的类型:
/**
workers是一个HashSet集合,它由coreSize/maximumPoolSize控制着,那么addWorker方法会导致OOM?结合实例代码demo,coreSize=maximumPoolSize=10,如果超过10,不会再添加到workers了,所以它不是导致newFixedThreadPool内存飙升的原因。那么,问题应该就在于workQueue.offer(command) 方法了。为了让整个流程清晰,我们画一下execute执行的流程图。
线程池执行方法execute的流程
根据以上execute以及addWork源码分析,我们把流程图画出来:
看完execute的执行流程,我猜测,内存飙升问题就是workQueue塞满了。接下来,进行阻塞队列源码分析,揭开内存飙升问题的神秘面纱。
阻塞队列源码分析
回到newFixedThreadPool构造函数,发现阻塞队列就是LinkedBlockingQueue,而且是个无参的LinkedBlockingQueue队列。OK,那我们直接分析LinkedBlockingQueue源码。
LinkedBlockingQueue类图
由类图可以看到:
LinkedBlockingQueue无参构造函数
public LinkedBlockingQueue() {this(Integer.MAX_VALUE);}public LinkedBlockingQueue(int capacity) {if (capacity <= 0) throw new IllegalArgumentException();this.capacity = capacity;last = head = new Node(null);}
LinkedBlockingQueue无参构造函数,默认构造Integer.MAX_VALUE(那么大) 的链表,看到这里,你回想一下execute流程,是不是阻塞队列一直不会满了,这队列来者不拒,把所有阻塞任务收于麾下。。。是不是内存飙升问题水落石出啦。
LinkedBlockingQueue的offer函数
线程池中,插入队列用了offer方法,我们来看一下阻塞队列LinkedBlockingQueue的offer骚操作吧
public boolean offer(E e) { //为空元素则抛出空指针异常 if (e == null) throw new NullPointerException(); final AtomicInteger count = this.count; //如采当前队列满则丢弃将要放入的元素, 然后返回false if (count.get() == capacity) return false; int c = -1; //构造新节点,获取putLock独占锁 Node<E> node = new Node<E>(e); final ReentrantLock putLock = this.putLock; putLock.lock(); try { //如采队列不满则进队列,并递增元素计数 if (count.get() < capacity) { enqueue(node); c = count.getAndIncrement(); //新元素入队后队列还有空闲空间,则 唤醒 notFull 的条件队列中一条阻塞线程 if (c + 1 < capacity) notFull.signal(); } } finally { //释放锁 putLock.unlock(); } if (c == 0) signalNotEmpty(); return c >= 0; }
offer操作向队列尾部插入一个元素,如果队列中有空闲则插入成功后返回 true,如果队列己满 则丢弃当前元素然后返回 false。 如果 e 元素为 null 则抛出 Nul!PointerException 异常。另外, 该方法是非阻塞的。
内存飙升问题结果揭晓
newFixedThreadPool线程池的核心线程数是固定的,它使用了近乎于无界的LinkedBlockingQueue阻塞队列。当核心线程用完后,任务会入队到阻塞队列,如果任务执行的时间比较长,没有释放,会导致越来越多的任务堆积到阻塞队列,最后导致机器的内存使用不停的飙升,造成JVM OOM。
声明:本站部分文章内容及图片转载于互联 、内容不代表本站观点,如有内容涉及侵权,请您立即联系本站处理,非常感谢!