內(nèi)存泄漏這種問題是可遇不可求的經(jīng)歷,終于有機(jī)會抓住了它,要好好的記錄下來。出現(xiàn)問題的是打成jar包的一個引擎程序
引擎邏輯
大致是生產(chǎn)者消費(fèi)者模式的一個數(shù)據(jù)處理引擎
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個生產(chǎn)者
Producer producer = new Producer(queue);
service.execute(producer);
//10個消費(fèi)者,每個消費(fèi)者加門閂,消費(fèi)完成減一
for (int i = 0; i < 10; i++) {
service.submit(new Consumer(queue,latch));
}
service.shutdown();
//主線程等待門閂,都完成后開始第二次循環(huán)
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}catch (Exception e){
}
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("循環(huán)一次結(jié)束,第二次開始調(diào)用");
main(new String[]{});
}
}
業(yè)務(wù)邏輯為生產(chǎn)者消費(fèi)者啟動,用CountDownLatch來阻塞住主線程,等所有消費(fèi)者生產(chǎn)者線程完成并結(jié)束后,main方法開始調(diào)用自己,開始第二次啟動,循環(huán)調(diào)用
這種情況下運(yùn)行一段時間后會出現(xiàn)異常:
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 以后已經(jīng)作為JDK 的一部分,位于 JDK 根目錄的 bin 文件夾下,無需安裝,直接運(yùn)行即可

打開后左側(cè)是所有的進(jìn)程,可以打開任意一個進(jìn)行詳細(xì)信息查看

右側(cè)對應(yīng)顯示詳細(xì)信息

分析程序崩潰時堆文件
程序運(yùn)行時,設(shè)置參數(shù)
-Xms200m
-Xmx200m
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=d:/dump/
設(shè)置最大內(nèi)存和指定OutOfMemoryError時存儲堆文件的位置
我們使用jvisualvm打開堆文件java_pid42132.hprof

占用內(nèi)存最大的是Obeject[]和byte[],并沒有顯示具體是哪個類導(dǎo)致的內(nèi)存問題,暫時無從下手。
猜想1:線程池的線程數(shù)過多導(dǎo)致
我們只能從程序邏輯來猜想這個問題了,由于程序多次回調(diào),很有可能是線程池里的線程未及時關(guān)閉導(dǎo)致的,我們修改代碼來驗證
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個生產(chǎn)者
Producer producer = new Producer(queue);
service.execute(producer);
//10個消費(fèi)者,每個消費(fèi)者加門閂,消費(fèi)完成減一
for (int i = 0; i < 10; i++) {
service.submit(new Consumer(queue,latch));
}
service.shutdown();
//主線程等待門閂,都完成后開始第二次循環(huán)
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
//輸出線程池狀態(tài)
System.out.println(service.toString());
}catch (Exception e){
}
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("循環(huán)一次結(jié)束,第二次開始調(diào)用");
main(new String[]{});
}
}
定義全局的線程池變量,每次輸出線程池狀態(tài)【長度,活動線程數(shù),完成線程數(shù)】
java.util.concurrent.ThreadPoolExecutor@720653c2[Running, pool size = 11, active threads = 0, queued tasks = 0, completed tasks = 11]
循環(huán)一次結(jié)束,第二次開始調(diào)用
java.util.concurrent.ThreadPoolExecutor@720653c2[Running, pool size = 11, active threads = 0, queued tasks = 0, completed tasks = 22]
循環(huán)一次結(jié)束,第二次開始調(diào)用
java.util.concurrent.ThreadPoolExecutor@720653c2[Running, pool size = 11, active threads = 0, queued tasks = 0, completed tasks = 33]
循環(huán)一次結(jié)束,第二次開始調(diào)用
java.util.concurrent.ThreadPoolExecutor@720653c2[Running, pool size = 11, active threads = 0, queued tasks = 0, completed tasks = 44]
循環(huán)一次結(jié)束,第二次開始調(diào)用
java.util.concurrent.ThreadPoolExecutor@720653c2[Running, pool size = 11, active threads = 0, queued tasks = 0, completed tasks = 55]
循環(huán)一次結(jié)束,第二次開始調(diào)用
java.util.concurrent.ThreadPoolExecutor@720653c2[Running, pool size = 11, active threads = 0, queued tasks = 0, completed tasks = 66]
循環(huán)一次結(jié)束,第二次開始調(diào)用
通過輸出可以看到:
存活線程數(shù)一直是0,當(dāng)前線程池長度為pool size=11,也就是剛執(zhí)行完的來不及釋放的1個生產(chǎn)者10個消費(fèi)者線程,已完成線程數(shù)completed tasks=11,22,33,44,55,66... 依次增長。
排除了線程池帶來的內(nèi)存溢出。
main方法無限回調(diào)導(dǎo)致的內(nèi)存問題
為了驗證這個猜想,設(shè)計代碼如下
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個生產(chǎn)者
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("循環(huán)一次結(jié)束,第二次開始調(diào)用");
main(new String[]{});
}
}
無限的new對象,無限的遞歸
通過jvisualvm進(jìn)行監(jiān)控,如下圖

可以看到內(nèi)存會周期性的進(jìn)行回收并保持良好狀態(tài),這個猜想也不正確。
client沒close()導(dǎo)致
最終通過代碼一塊塊的邏輯排除法得出結(jié)論:
是生產(chǎn)者和消費(fèi)者中的連接Elasticsearch的Client使用完畢后,雖然線程關(guān)閉了,但是client沒有關(guān)閉導(dǎo)致的
通過jvisualvm也可以發(fā)現(xiàn)一些線索,我們使用jvisualvm打開堆文件java_pid42132.hprof

雙擊打開
java.lang.Object[]可以查看它的組成
一級一級的跟下去會發(fā)現(xiàn)有elasticsearch——client的影子
最后
解決方法很簡單:線程結(jié)束時,關(guān)閉該線程使用的client客戶端
elasticServer.client.close();
System.out.println("consumer end!");
latch.countDown();
我們要注意的就是在數(shù)據(jù)庫連接的處理上要額外注意,一般情況下不會出問題,在頻繁的連接釋放和遞歸時,很有可能引起內(nèi)存泄漏。