線上服務(wù)內(nèi)存溢出
這周剛上班突然有一個(gè)項(xiàng)目內(nèi)存溢出了,排查了半天終于找到問題所在,在此記錄下,防止后面再次出現(xiàn)類似的情況。
先簡單說下當(dāng)出現(xiàn)內(nèi)存溢出之后,我是如何排查的,首先通過jstack打印出堆棧信息,然后通過分析工具對這些文件進(jìn)行分析,根據(jù)分析結(jié)果我們就可以知道大概是由于什么問題引起的。
關(guān)于jstack如何使用,大家可以先看看這篇文章 jstack的使用
問題排查
下面是我打印出來的信息,大部分都是這個(gè)
"http-nio-8761-exec-124" #580 daemon prio=5 os_prio=0 tid=0x00007fbd980c0800 nid=0x249 waiting on condition [0x00007fbcf09c8000]
java.lang.Thread.State: TIMED_WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000000f73a4508> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:215)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2078)
at java.util.concurrent.LinkedBlockingQueue.poll(LinkedBlockingQueue.java:467)
at org.apache.tomcat.util.threads.TaskQueue.poll(TaskQueue.java:85)
at org.apache.tomcat.util.threads.TaskQueue.poll(TaskQueue.java:31)
at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1073)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)
看到了如上信息之后,大概可以看出是由于線程池的使用不當(dāng)導(dǎo)致的,那么根據(jù)信息繼續(xù)往下看,看到ThreadPoolExecutor那么就可以知道這肯定是創(chuàng)建了線程池,那么我們就在代碼里找,哪里創(chuàng)建使用了線程池,我就找到這么一段代碼。
public class ThreadPool {
private static ExecutorService pool;
private static long logTime = 0;
public static ExecutorService getPool() {
if (pool == null) {
pool = Executors.newFixedThreadPool(20);
}
return pool;
}
}
乍一看,可能寫的同學(xué)是想把這當(dāng)一個(gè)全局的線程池用,所有的業(yè)務(wù)凡是用到線程的都會(huì)使用這個(gè)類,為了統(tǒng)一管理線程,想法沒什么毛病,但是這樣寫確實(shí)有點(diǎn)子毛病。
newFixedThreadPool分析
上面使用了Executors.newFixedThreadPool(20)創(chuàng)建了一個(gè)固定的線程池,我們先分析下newFixedThreadPool是怎么樣的一個(gè)流程。

一個(gè)請求進(jìn)來之后,如果核心線程有空閑線程直接使用核心線程中的線程執(zhí)行任務(wù),不會(huì)添加到阻塞隊(duì)列中,如果核心線程滿了,新的任務(wù)會(huì)添加到阻塞隊(duì)列,直到隊(duì)列加滿再開線程,直到maxPoolSize之后再觸發(fā)拒絕執(zhí)行策略
了解了流程之后我們再來看newFixedThreadPool的代碼實(shí)現(xiàn)。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
// 任務(wù)阻塞隊(duì)列的初始容量
this.capacity = capacity;
last = head = new Node<E>(null);
}
定位問題
看到了這里不知道你是否知道了此次引起內(nèi)存泄漏的原因,其實(shí)就是因?yàn)?strong>阻塞隊(duì)列的容量過大。
如果不手動(dòng)的指定阻塞隊(duì)列的大小,那么它默認(rèn)是Integer.MAX_VALUE,我們的線程池只有20個(gè)線程可以處理任務(wù),其他的請求全部放到阻塞隊(duì)列中,那么當(dāng)涌入大量的請求之后,阻塞隊(duì)列一直增加,你的內(nèi)存配置又非常緊湊的話,那么是很容易出現(xiàn)內(nèi)存溢出的。
我們的業(yè)務(wù)是在APP啟動(dòng)的時(shí)候,會(huì)使用線程池去檢查用戶的一些配置,應(yīng)用的啟動(dòng)量還是非常大的而且給的內(nèi)存配置也不是很足,所以運(yùn)行一段時(shí)間后,部分容器就出現(xiàn)了內(nèi)存溢出的情況。
如何正確的創(chuàng)建線程池
以前其實(shí)沒太在意這種問題,都是使用Executors去創(chuàng)建線程,但是這樣確實(shí)會(huì)存在一些問題,就像這些的內(nèi)存泄漏,所以一般不要使用Executors去創(chuàng)建線程,使用ThreadPoolExecutor進(jìn)行創(chuàng)建,其實(shí)Executors底層也是使用ThreadPoolExecutor進(jìn)行創(chuàng)建的。
使用ThreadPoolExecutor創(chuàng)建需要自己指定核心線程數(shù)、最大線程數(shù)、線程的空閑時(shí)長以及阻塞隊(duì)列。
3種阻塞隊(duì)列
- ArrayBlockingQueue:基于數(shù)組的先進(jìn)先出隊(duì)列,有界
- LinkedBlockingQueue:基于鏈表的先進(jìn)先出隊(duì)列,有界
- SynchronousQueue:無緩沖的等待隊(duì)列,無界
我們使用了有界的隊(duì)列,那么當(dāng)隊(duì)列滿了之后如何處理后面進(jìn)入的請求,我們可以通過不同的策略進(jìn)行設(shè)置。
4種拒絕策略
- AbortPolicy:默認(rèn),隊(duì)列滿了丟任務(wù)拋出異常
- DiscardPolicy:隊(duì)列滿了丟任務(wù)不異常
- DiscardOldestPolicy:將最早進(jìn)入隊(duì)列的任務(wù)刪,之后再嘗試加入隊(duì)列
- CallerRunsPolicy:如果添加到線程池失敗,那么主線程會(huì)自己去執(zhí)行該任務(wù)
在創(chuàng)建之前,先說下我最開始的版本,因?yàn)殛?duì)列是固定的,最開始我們不知道有拒絕策略,所以在隊(duì)列滿了之后再添加的話會(huì)出現(xiàn)異常,我就在異常里面睡眠了1秒,等待其他的線程執(zhí)行完畢獲取空閑連接,但是還是會(huì)有部分不能得到執(zhí)行。
接下來我們來創(chuàng)建一個(gè)容錯(cuò)率比較高的線程池。
public class WordTest {
public static void main(String[] args) throws InterruptedException {
System.out.println("開始執(zhí)行");
// 阻塞隊(duì)列容量聲明為100個(gè)
ThreadPoolExecutor executorService = new ThreadPoolExecutor(10, 10,
0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(100));
// 設(shè)置拒絕策略
executorService.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 空閑隊(duì)列存活時(shí)間
executorService.setKeepAliveTime(20, TimeUnit.SECONDS);
List<Integer> list = new ArrayList<>(2000);
try {
// 模擬200個(gè)請求
for (int i = 0; i < 200; i++) {
final int num = i;
executorService.execute(() -> {
System.out.println(Thread.currentThread().getName() + "-結(jié)果:" + num);
list.add(num);
});
}
} finally {
executorService.shutdown();
executorService.awaitTermination(10, TimeUnit.SECONDS);
}
System.out.println("線程執(zhí)行結(jié)束");
}
}
思路:我聲明了100容量的阻塞隊(duì)列,模擬了一個(gè)200的請求,很顯然肯定有部分請求進(jìn)入不了隊(duì)列,但是我使用了CallerRunsPolicy策略,當(dāng)隊(duì)列滿了之后,使用主線程去進(jìn)行處理,這樣就不會(huì)出現(xiàn)有部分請求得不到執(zhí)行的情況,也不會(huì)因?yàn)橐驗(yàn)樽枞?duì)列過大導(dǎo)致內(nèi)存溢出的情況。
如果還有什么更好地寫法歡迎各位指教!
通過測試200個(gè)請求全部得到執(zhí)行,有3個(gè)請求由主線程進(jìn)行了處理。
總結(jié)
如何更好的創(chuàng)建線程池上面已經(jīng)說過了,關(guān)于線程池在業(yè)務(wù)中的使用,其實(shí)我們這種全局的思路是不太好的,因?yàn)槿绻麖娜挚紤]去創(chuàng)建線程池,是很難把控的,因?yàn)槟銦o法準(zhǔn)確地評估所有的請求加起來會(huì)有多大的量,所以最好是每個(gè)業(yè)務(wù)創(chuàng)建獨(dú)立的線程池進(jìn)行處理,這樣是很容易評估量化的。
另外創(chuàng)建的時(shí)候,最好評估下大概每秒的請求量有多少,然后來合理的初始化線程數(shù)和隊(duì)列大小。
更多精彩內(nèi)容請關(guān)注微信公眾號(hào):一個(gè)程序員的成長