线程池原因导致java.lang.OutOfMemoryError
线程池原因导致java.lang.OutOfMemoryError
July 19, 2021
问题描述
线上环境某个服务经常性地抛出内存溢出,看日志是下面的错误
java.lang.OutOfMemoryError: unable to create new native thread
at java.lang.Thread.start0(Native Method) ~[?:1.8.0_112]
at java.lang.Thread.start(Thread.java:714) ~[?:1.8.0_112]
at java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:950) ~[?:1.8.0_112]
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1357) ~[?:1.8.0_112]
at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:134) ~[?:1.8.0_112]
造成这个错误主要有2个原因:
- 剩余的系统内存不足,导致无法创建新的线程
- 总线程数达到操作系统允许的上限
看了代码后,发现开发人员把线程池作为一个方法的局部变量,由于这方法是被定时任务调用的,也就意味着线程池局部变量会被实例化N次,如果线程池没有被回收,那么最终总线程数会达到操作系统的上限。
问题剖析
当对象实例不再使用或者方法执行完毕后,什么时候会释放线程与关闭线程池?不同的线程池的实现方式可能不一样,但是主要还是看是否设置了核心线程数。
- 如果没有设置核心线程数,比如 newCachedThreadPool ,在线程池的线程空闲时间到达 60s 后,线程会关闭,所有线程关闭后线程池也相应关闭回收。
- 如果设置了核心线程数,比如 newSingleThreadExecutor 和 newFixedThreadPool,如果没有主动去关闭,或者设置核心线程的超时时间,核心线程会一直存在不会被关闭,这个线程池就不会被释放回收。
可以通过下面的ThreadPoolExecutor
源码中runWorker
方法,看到要执行线程退出processWorkerExit
需要这几种情况:
- 线程池的状态 >= STOP。对于这个情况,线程池的状态要达到 STOP,需要调用
shutdown
或者shutdownNow
方法 - getTask 获取到空任务
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);
}
}
对于第二种情况,先看看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;
}
}
}
从方法中可以看到:
- 当前线程数大于核心线程,会调用
poll
超时后返回空任务。 - 当前线程数小于等于核心线程,并且调用了
allowCoreThreadTimeOut
方法允许核心线程超时关闭的情况下,也是调用poll
,超时后返回空任务。 - 其他情况,调用
take
阻塞等待。
任务队列以阻塞队列BlockingQueue
为例,该队列提供了两种方法来获取任务:
- poll,可以设置超时时间,当超时后会得到一个空任务。
- take,阻塞住,直到有任务出现。
在没有任务的情况下,核心线程正处于getTask
,调用阻塞队列BlockingQueue
的 take
方法阻塞等待获取到任务,从而导致线程池包括里面的核心线程迟迟不被关闭并且回收。
小结
不推荐将线程池作为局部变量使用,而要作为全局变量。一般都会把线程池作为类的静态成员或者单例成员,毕竟生命周期和进程一致。
如果业务场景非要这样用的话,并且线程池有核心线程的情况下,要注意做两件事情防止对象泄漏:
- 对核心线程设置超时时间。
- 主动调用 shutdown 或 shutdownNow 来关闭线程池。
示例:
public class TestThread {
public static void main(String[] args) {
while (true) {
ExecutorService service = Executors.newFixedThreadPool(1);
try {
service.submit(new Runnable() {
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
}
});
} catch (Exception e) {
}finally{
// 调用shutdown来关闭
service.shutdown();
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
}
}
}
参考:
最后更新于