這周剛上班忽然有一個項目內存溢出了,排查了半天終於找到問題所在,在此記錄下,防止後面再次出現相似的狀況。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
上面使用了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建立須要本身指定核心線程數、最大線程數、線程的空閒時長以及阻塞隊列。
咱們使用了有界的隊列,那麼當隊列滿了以後如何處理後面進入的請求,咱們能夠經過不一樣的策略進行設置。
在建立以前,先說下我最開始的版本,由於隊列是固定的,最開始咱們不知道有拒絕策略,因此在隊列滿了以後再添加的話會出現異常,我就在異常裏面睡眠了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...
更多精彩內容請關注微信公衆號:一個程序員的成長