Executors使用不當引發的內存溢出

線上服務內存溢出

這周剛上班忽然有一個項目內存溢出了,排查了半天終於找到問題所在,在此記錄下,防止後面再次出現相似的狀況。html

先簡單說下當出現內存溢出以後,我是如何排查的,首先經過jstack打印出堆棧信息,而後經過分析工具對這些文件進行分析,根據分析結果咱們就能夠知道大概是因爲什麼問題引發的。java

關於jstack如何使用,你們能夠先看看這篇文章 jstack的使用程序員

問題排查

下面是我打印出來的信息,大部分都是這個apache

"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)

看到了如上信息以後,大概能夠看出是因爲線程池的使用不當致使的,那麼根據信息繼續往下看,看到ThreadPoolExecutor那麼就能夠知道這確定是建立了線程池,那麼咱們就在代碼裏找,哪裏建立使用了線程池,我就找到這麼一段代碼。數組

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;
    }
}

乍一看,可能寫的同窗是想把這當一個全局的線程池用,全部的業務凡是用到線程的都會使用這個類,爲了統一管理線程,想法沒什麼毛病,可是這樣寫確實有點子毛病。tomcat

newFixedThreadPool分析

上面使用了Executors.newFixedThreadPool(20)建立了一個固定的線程池,咱們先分析下newFixedThreadPool是怎麼樣的一個流程。微信

一個請求進來以後,若是核心線程有空閒線程直接使用核心線程中的線程執行任務,不會添加到阻塞隊列中,若是核心線程滿了,新的任務會添加到阻塞隊列,直到隊列加滿再開線程,直到maxPoolSize以後再觸發拒絕執行策略app

瞭解了流程以後咱們再來看newFixedThreadPool的代碼實現。工具

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();
    // 任務阻塞隊列的初始容量
    this.capacity = capacity;
    last = head = new Node<E>(null);
}

定位問題

看到了這裏不知道你是否知道了這次引發內存泄漏的緣由,其實就是由於阻塞隊列的容量過大測試

若是不手動的指定阻塞隊列的大小,那麼它默認是Integer.MAX_VALUE,咱們的線程池只有20個線程能夠處理任務,其餘的請求所有放到阻塞隊列中,那麼當涌入大量的請求以後,阻塞隊列一直增長,你的內存配置又很是緊湊的話,那麼是很容易出現內存溢出的。

咱們的業務是在APP啓動的時候,會使用線程池去檢查用戶的一些配置,應用的啓動量仍是很是大的並且給的內存配置也不是很足,因此運行一段時間後,部分容器就出現了內存溢出的狀況。

如何正確的建立線程池

之前其實沒太在乎這種問題,都是使用Executors去建立線程,可是這樣確實會存在一些問題,就像這些的內存泄漏,因此通常不要使用Executors去建立線程,使用ThreadPoolExecutor進行建立,其實Executors底層也是使用ThreadPoolExecutor進行建立的。

使用ThreadPoolExecutor建立須要本身指定核心線程數、最大線程數、線程的空閒時長以及阻塞隊列。

3種阻塞隊列
  • ArrayBlockingQueue:基於數組的先進先出隊列,有界
  • LinkedBlockingQueue:基於鏈表的先進先出隊列,有界
  • SynchronousQueue:無緩衝的等待隊列,無界

咱們使用了有界的隊列,那麼當隊列滿了以後如何處理後面進入的請求,咱們能夠經過不一樣的策略進行設置。

4種拒絕策略
  • AbortPolicy:默認,隊列滿了丟任務拋出異常
  • DiscardPolicy:隊列滿了丟任務不異常
  • DiscardOldestPolicy:將最先進入隊列的任務刪,以後再嘗試加入隊列
  • CallerRunsPolicy:若是添加到線程池失敗,那麼主線程會本身去執行該任務
在建立以前,先說下我最開始的版本,由於隊列是固定的,最開始咱們不知道有拒絕策略,因此在隊列滿了以後再添加的話會出現異常,我就在異常裏面睡眠了1秒,等待其餘的線程執行完畢獲取空閒鏈接,可是仍是會有部分不能獲得執行。

接下來咱們來建立一個容錯率比較高的線程池。

public class WordTest {

    public static void main(String[] args) throws InterruptedException {

        System.out.println("開始執行");

        // 阻塞隊列容量聲明爲100個
        ThreadPoolExecutor executorService = new ThreadPoolExecutor(10, 10,
                0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(100));

        // 設置拒絕策略
        executorService.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        // 空閒隊列存活時間
        executorService.setKeepAliveTime(20, TimeUnit.SECONDS);

        List<Integer> list = new ArrayList<>(2000);

        try {
            // 模擬200個請求
            for (int i = 0; i < 200; i++) {
                final int num = i;
                executorService.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "-結果:" + num);
                    list.add(num);
                });
            }
        } finally {
            executorService.shutdown();
            executorService.awaitTermination(10, TimeUnit.SECONDS);
        }
        System.out.println("線程執行結束");
    }
}

思路:我聲明瞭100容量的阻塞隊列,模擬了一個200的請求,很顯然確定有部分請求進入不了隊列,可是我使用了CallerRunsPolicy策略,當隊列滿了以後,使用主線程去進行處理,這樣就不會出現有部分請求得不到執行的狀況,也不會由於由於阻塞隊列過大致使內存溢出的狀況。

若是還有什麼更好地寫法歡迎各位指教!

經過測試200個請求所有獲得執行,有3個請求由主線程進行了處理。

總結

如何更好的建立線程池上面已經說過了,關於線程池在業務中的使用,其實咱們這種全局的思路是不太好的,由於若是從全局考慮去建立線程池,是很難把控的,由於你沒法準確地評估全部的請求加起來會有多大的量,因此最好是每一個業務建立獨立的線程池進行處理,這樣是很容易評估量化的。

另外建立的時候,最好評估下大概每秒的請求量有多少,而後來合理的初始化線程數和隊列大小。

參考文章:<br/>
https://www.cnblogs.com/muxi0...

更多精彩內容請關注微信公衆號:一個程序員的成長

相關文章
相關標籤/搜索