內存泄漏這種問題是可遇不可求的經歷,終于有機會抓住了它,要好好的記錄下來。出現問題的是打成jar包的一個引擎程序
引擎邏輯
大致是生產者消費者模式的一個數據處理引擎
public class MainClass {
public static void main(String[] args) {
try {
//定義 線程池、隊列、門閂
ExecutorService service = Executors.newCachedThreadPool();
BlockingQueue<JSONObject> queue = new LinkedBlockingQueue<JSONObject>(100);
CountDownLatch latch = new CountDownLatch(10);
//1個生產者
Producer producer = new Producer(queue);
service.execute(producer);
//10個消費者,每個消費者加門閂,消費完成減一
for (int i = 0; i < 10; i++) {
service.submit(new Consumer(queue,latch));
}
service.shutdown();
//主線程等待門閂,都完成后開始第二次循環
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}catch (Exception e){
}
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("循環一次結束,第二次開始調用");
main(new String[]{});
}
}
業務邏輯為生產者消費者啟動,用CountDownLatch來阻塞住主線程,等所有消費者生產者線程完成并結束后,main方法開始調用自己,開始第二次啟動,循環調用
這種情況下運行一段時間后會出現異常:
Caused by: java.lang.OutOfMemoryError: unable to create new native thread
at java.lang.Thread.start0(Native Method)
at java.lang.Thread.start(Thread.java:717)
at java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:957)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1367)
at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:112)
針對OutOfMemoryError
異常我們使用jdk自帶的工具jvisualvm
來查看
jvisualvm使用
jvisualvm自從 JDK 6 Update 7 以后已經作為JDK 的一部分,位于 JDK 根目錄的 bin 文件夾下,無需安裝,直接運行即可
打開后左側是所有的進程,可以打開任意一個進行詳細信息查看
右側對應顯示詳細信息
分析程序崩潰時堆文件
程序運行時,設置參數
-Xms200m
-Xmx200m
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=d:/dump/
設置最大內存和指定OutOfMemoryError時存儲堆文件的位置
我們使用jvisualvm打開堆文件java_pid42132.hprof
占用內存最大的是Obeject[]和byte[],并沒有顯示具體是哪個類導致的內存問題,暫時無從下手。
猜想1:線程池的線程數過多導致
我們只能從程序邏輯來猜想這個問題了,由于程序多次回調,很有可能是線程池里的線程未及時關閉導致的,我們修改代碼來驗證
public class MainClass {
//全局線程池
static ExecutorService service = Executors.newCachedThreadPool();
public static void main(String[] args) {
try {
//定義 線程池、隊列、門閂
BlockingQueue<JSONObject> queue = new LinkedBlockingQueue<JSONObject>(100);
CountDownLatch latch = new CountDownLatch(10);
//1個生產者
Producer producer = new Producer(queue);
service.execute(producer);
//10個消費者,每個消費者加門閂,消費完成減一
for (int i = 0; i < 10; i++) {
service.submit(new Consumer(queue,latch));
}
service.shutdown();
//主線程等待門閂,都完成后開始第二次循環
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
//輸出線程池狀態
System.out.println(service.toString());
}catch (Exception e){
}
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("循環一次結束,第二次開始調用");
main(new String[]{});
}
}
定義全局的線程池變量,每次輸出線程池狀態【長度,活動線程數,完成線程數】
java.util.concurrent.ThreadPoolExecutor@720653c2[Running, pool size = 11, active threads = 0, queued tasks = 0, completed tasks = 11]
循環一次結束,第二次開始調用
java.util.concurrent.ThreadPoolExecutor@720653c2[Running, pool size = 11, active threads = 0, queued tasks = 0, completed tasks = 22]
循環一次結束,第二次開始調用
java.util.concurrent.ThreadPoolExecutor@720653c2[Running, pool size = 11, active threads = 0, queued tasks = 0, completed tasks = 33]
循環一次結束,第二次開始調用
java.util.concurrent.ThreadPoolExecutor@720653c2[Running, pool size = 11, active threads = 0, queued tasks = 0, completed tasks = 44]
循環一次結束,第二次開始調用
java.util.concurrent.ThreadPoolExecutor@720653c2[Running, pool size = 11, active threads = 0, queued tasks = 0, completed tasks = 55]
循環一次結束,第二次開始調用
java.util.concurrent.ThreadPoolExecutor@720653c2[Running, pool size = 11, active threads = 0, queued tasks = 0, completed tasks = 66]
循環一次結束,第二次開始調用
通過輸出可以看到:
存活線程數一直是0,當前線程池長度為pool size=11,也就是剛執行完的來不及釋放的1個生產者10個消費者線程,已完成線程數completed tasks=11,22,33,44,55,66... 依次增長。
排除了線程池帶來的內存溢出。
main方法無限回調導致的內存問題
為了驗證這個猜想,設計代碼如下
public class MainClass {
public static void main(String[] args) {
try {
//定義 線程池、隊列、門閂
ExecutorService service = Executors.newCachedThreadPool();
BlockingQueue<JSONObject> queue = new LinkedBlockingQueue<JSONObject>(100);
CountDownLatch latch = new CountDownLatch(10);
//new 10個生產者
for(int i=0;i<10;i++){
Producer producer = new Producer(queue);
}
}catch (Exception e){
}
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("循環一次結束,第二次開始調用");
main(new String[]{});
}
}
無限的new對象,無限的遞歸
通過jvisualvm進行監控,如下圖
可以看到內存會周期性的進行回收并保持良好狀態,這個猜想也不正確。
client沒close()導致
最終通過代碼一塊塊的邏輯排除法得出結論:
是生產者和消費者中的連接Elasticsearch的Client使用完畢后,雖然線程關閉了,但是client沒有關閉導致的
通過jvisualvm
也可以發現一些線索,我們使用jvisualvm
打開堆文件java_pid42132.hprof
雙擊打開
java.lang.Object[]
可以查看它的組成一級一級的跟下去會發現有elasticsearch——client的影子
最后
解決方法很簡單:線程結束時,關閉該線程使用的client客戶端
elasticServer.client.close();
System.out.println("consumer end!");
latch.countDown();
我們要注意的就是在數據庫連接的處理上要額外注意,一般情況下不會出問題,在頻繁的連接釋放和遞歸時,很有可能引起內存泄漏。