记一次线上JVM无法创建新线程OutOfMemoryError: unable to create new native thread

175 阅读5分钟

错误:org.springframework.web.util.NestedServletException: Handler dispatch failed; nested exception is java.lang.OutOfMemoryError: unable to create new native thread

使用线程池时线程数量一直居高不下,导致jvm不能创建新线程。

图片.png

ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(100,300,100, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>());
poolExecutor.execute(()->{
//相关业务代码;
}

1.初步怀疑是线程池里面的线程和线程池没有得到释放

通过网上的答案,是因为没有使用shutdown进行释放。 下面进行代码测试: (1)不使用shutdown

public static void main(String[] args) throws InterruptedException {
    while(true){
        threadDontGcDemo();
        Thread.sleep(2000);

    }
}

private static void threadDontGcDemo(){
    ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(100,300,100, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>());
    poolExecutor.execute(()-> {
        System.out.println("11111");
    });

}

通过jvm监控工具监控线程变化,可以看到线程数和线程池一直在增加,并没有被回收,确实符合发生的问题状况

Snipaste_2023-08-02_10-29-14.png

(2)使用shutdown

public static void main(String[] args) throws InterruptedException {
    while(true){
        threadDontGcDemo();
        Thread.sleep(2000);

    }
}

private static void threadDontGcDemo(){
    ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(100,300,100, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>());
    poolExecutor.execute(()-> {
        System.out.println("11111");
    });
    poolExecutor.shutdown();

}

结果是线程和线程池都被回收了。执行了shutdown的线程池最后会回收线程池和线程对象

Snipaste_2023-08-02_10-31-00.png

一个对象能不能回收,是看它到gc root之间有没有可达路径,线程池不能回收说明到达线程池的gc root还是有可达路径的。这里的线程池的gc root是线程,具体的gc路径是thread->workers->线程池

线程对象是线程池的gc root,假如线程对象能被gc,那么线程池对象肯定也能被gc掉(因为线程池对象已经没有到gc root的可达路径了)。

2.通过shutdown方法的源码查看线程对象是在什么时候被回收的

public void shutdown() {  
    final ReentrantLock mainLock = this.mainLock;  
    mainLock.lock();  
    try {  
        checkShutdownAccess();  
        advanceRunState(SHUTDOWN);  
        interruptIdleWorkers();  
        onShutdown(); // hook for ScheduledThreadPoolExecutor  
    } finally {  
        mainLock.unlock();  
    }  
    tryTerminate();  
}  
  
private void interruptIdleWorkers() {  
    interruptIdleWorkers(false);  
}  
  
private void interruptIdleWorkers(boolean onlyOne) {  
    final ReentrantLock mainLock = this.mainLock;  
    mainLock.lock();  
    try {  
        for (Worker w : workers) {  
            Thread t = w.thread;  
            if (!t.isInterrupted() && w.tryLock()) {  
                try {  
                    t.interrupt();  
                } catch (SecurityException ignore) {  
                } finally {  
                    w.unlock();  
                }  
            }  
            if (onlyOne)  
                break;  
        }  
    } finally {  
        mainLock.unlock();  
    }  
}

interruptIdleWorkers方法里面主要就做了一件事,遍历当前线程池中的线程,并且调用线程的interrupt()方法,通知线程中断,也就是说shutdown方法只是去遍历所有线程池中的线程,然后通知线程中断。所以需要了解线程池里的线程是怎么处理中断的通知的。

点开worker对象,这个worker对象是线程池中实际运行的线程,直接看worker的run方法,中断通知是在里面被处理了

final void runWorker(Worker w) {  
    Thread wt = Thread.currentThread();  
    Runnable task = w.firstTask;  
    w.firstTask = null;  
    w.unlock(); // allow interrupts  
    boolean completedAbruptly = true;  
    try {  
        while (task != null || (task = getTask()) != null) {  
            w.lock();  
            // If pool is stopping, ensure thread is interrupted;  
            // if not, ensure thread is not interrupted.  This  
            // requires a recheck in second case to deal with  
            // shutdownNow race while clearing interrupt  
            if ((runStateAtLeast(ctl.get(), STOP) ||  
                 (Thread.interrupted() &&  
                  runStateAtLeast(ctl.get(), STOP))) &&  
                !wt.isInterrupted())  
                wt.interrupt();  
            try {  
                beforeExecute(wt, task);  
                Throwable thrown = null;  
                try {  
                    task.run();  
                } catch (RuntimeException x) {  
                    thrown = x; throw x;  
                } catch (Error x) {  
                    thrown = x; throw x;  
                } catch (Throwable x) {  
                    thrown = x; throw new Error(x);  
                } finally {  
                    afterExecute(task, thrown);  
                }  
            } finally {  
                task = null;  
                w.completedTasks++;  
                w.unlock();  
            }  
        }  
        completedAbruptly = false;  
    } finally {  
        processWorkerExit(w, completedAbruptly);  
    }  
}

runwoker属于线程池的核心方法,线程池能不断运作的原理就是这里。

最外层用一个while循环套住,然后不断的调用gettask()方法不断从队列中取任务,若拿不到任务或者任务执行发生异常(抛出异常了)那就属于异常情况,直接将completedAbruptly 设置为true,并且进入异常的processWorkerExit流程。

了解gettask()方法什么时候可能会抛出异常:

private Runnable getTask() {  
    boolean timedOut = false; // Did the last poll() time out?  
  
    for (;;) {  
        int c = ctl.get();  
        int rs = runStateOf(c);  
  
        // Check if queue empty only if necessary.  
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {  
            decrementWorkerCount();  
            return null;  
        }  
  
        int wc = workerCountOf(c);  
  
        // Are workers subject to culling?  
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;  
  
        if ((wc > maximumPoolSize || (timed && timedOut))  
            && (wc > 1 || workQueue.isEmpty())) {  
            if (compareAndDecrementWorkerCount(c))  
                return null;  
            continue;  
        }  
  
        try {  
            Runnable r = timed ?  
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :  
            workQueue.take();  
            if (r != null)  
                return r;  
            timedOut = true;  
        } catch (InterruptedException retry) {  
            timedOut = false;  
        }  
    }  
}

gettask就是从工作队列中取任务,但是前面还有个timed,这个timed的语义是:如果allowCoreThreadTimeOut参数为true(一般为false)或者当前工作线程数超过核心线程数,那么使用队列的poll方法取任务,反之使用take方法。

重点是poll方法和take方法都会让当前线程进入time_waiting或者waiting状态。而当线程处于在等待状态的时候,调用线程的interrupt方法,会使线程当场抛出异常!

即线程池的shutdownnow方法调用interruptIdleWorkers去对线程对象interrupt是为了让处于waiting或者是time_waiting的线程抛出异常。

线程池处理这个异常的地址是runwoker中的调用的processWorkerExit方法:

private void processWorkerExit(Worker w, boolean completedAbruptly) {  
    if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted  
        decrementWorkerCount();  
  
    final ReentrantLock mainLock = this.mainLock;  
    mainLock.lock();  
    try {  
        completedTaskCount += w.completedTasks;  
        workers.remove(w);  
    } finally {  
        mainLock.unlock();  
    }  
  
    tryTerminate();  
  
    int c = ctl.get();  
    if (runStateLessThan(c, STOP)) {  
        if (!completedAbruptly) {  
            int min = allowCoreThreadTimeOut ? 0 : corePoolSize;  
            if (min == 0 && ! workQueue.isEmpty())  
                min = 1;  
            if (workerCountOf(c) >= min)  
                return; // replacement not needed  
        }  
        addWorker(null, false);  
    }  
}

在这个方法里有个明显的workers.remove(w)方法,这里w的变量,被移出了workers这个集合,导致worker对象不能到达gc root,于是workder对象变成了一个垃圾对象,被回收掉。

然后等到worker中所有的worker都被移出works后,并且当前请求线程也完成后,线程池对象也成为了一个孤儿对象,没办法到达gc root,于是线程池对象也被gc掉。

3.总结

  • 线程池调用shutdownnow方法是为了调用worker对象的interrupt方法,来打断那些沉睡中的线程(waiting或者time_waiting状态),使其抛出异常
  • 线程池会把抛出异常的worker对象从workers集合中移除引用,此时被移除的worker对象因为没有到达gc root的路径已经可以被gc掉了
  • 等到workers对象空了,并且当前tomcat线程也结束,此时线程池对象也可以被gc掉,整个线程池对象成功释放

在局部方法中使用线程池,线程池对象不是bean的情况时,要合理使用shutdown或者shutdownnow方法来释放线程和线程池对象,若不使用,将会造成线程池和线程对象的堆积。