Executors使用不當(dāng)引起的內(nèi)存溢出

線上服務(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è)流程。

image

一個(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ì)列大小。

參考文章:

https://www.cnblogs.com/muxi0407/p/11811669.html

更多精彩內(nèi)容請關(guān)注微信公眾號(hào):一個(gè)程序員的成長

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容