內(nèi)存泄漏這種問題是可遇不可求的經(jīng)歷历极,終于有機(jī)會(huì)抓住了它窄瘟,要好好的記錄下來。出現(xiàn)問題的是打成jar包的一個(gè)引擎程序
引擎邏輯
大致是生產(chǎn)者消費(fèi)者模式的一個(gè)數(shù)據(jù)處理引擎
public class MainClass {
public static void main(String[] args) {
try {
//定義 線程池趟卸、隊(duì)列蹄葱、門閂
ExecutorService service = Executors.newCachedThreadPool();
BlockingQueue<JSONObject> queue = new LinkedBlockingQueue<JSONObject>(100);
CountDownLatch latch = new CountDownLatch(10);
//1個(gè)生產(chǎn)者
Producer producer = new Producer(queue);
service.execute(producer);
//10個(gè)消費(fèi)者,每個(gè)消費(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)者啟動(dòng)邻邮,用CountDownLatch來阻塞住主線程竣况,等所有消費(fèi)者生產(chǎn)者線程完成并結(jié)束后,main方法開始調(diào)用自己筒严,開始第二次啟動(dòng)丹泉,循環(huán)調(diào)用
這種情況下運(yùn)行一段時(shí)間后會(huì)出現(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)
針對(duì)OutOfMemoryError
異常我們使用jdk自帶的工具jvisualvm
來查看
jvisualvm使用
jvisualvm自從 JDK 6 Update 7 以后已經(jīng)作為JDK 的一部分,位于 JDK 根目錄的 bin 文件夾下鸭蛙,無需安裝摹恨,直接運(yùn)行即可
打開后左側(cè)是所有的進(jìn)程,可以打開任意一個(gè)進(jìn)行詳細(xì)信息查看
右側(cè)對(duì)應(yīng)顯示詳細(xì)信息
分析程序崩潰時(shí)堆文件
程序運(yùn)行時(shí)娶视,設(shè)置參數(shù)
-Xms200m
-Xmx200m
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=d:/dump/
設(shè)置最大內(nèi)存和指定OutOfMemoryError時(shí)存儲(chǔ)堆文件的位置
我們使用jvisualvm打開堆文件java_pid42132.hprof
占用內(nèi)存最大的是Obeject[]和byte[]晒哄,并沒有顯示具體是哪個(gè)類導(dǎo)致的內(nèi)存問題,暫時(shí)無從下手。
猜想1:線程池的線程數(shù)過多導(dǎo)致
我們只能從程序邏輯來猜想這個(gè)問題了揩晴,由于程序多次回調(diào)勋陪,很有可能是線程池里的線程未及時(shí)關(guān)閉導(dǎo)致的贪磺,我們修改代碼來驗(yàn)證
public class MainClass {
//全局線程池
static ExecutorService service = Executors.newCachedThreadPool();
public static void main(String[] args) {
try {
//定義 線程池硫兰、隊(duì)列、門閂
BlockingQueue<JSONObject> queue = new LinkedBlockingQueue<JSONObject>(100);
CountDownLatch latch = new CountDownLatch(10);
//1個(gè)生產(chǎn)者
Producer producer = new Producer(queue);
service.execute(producer);
//10個(gè)消費(fèi)者寒锚,每個(gè)消費(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)【長度,活動(dòng)線程數(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個(gè)生產(chǎn)者10個(gè)消費(fèi)者線程莫辨,已完成線程數(shù)completed tasks=11,22,33,44,55,66... 依次增長傲茄。
排除了線程池帶來的內(nèi)存溢出。
main方法無限回調(diào)導(dǎo)致的內(nèi)存問題
為了驗(yàn)證這個(gè)猜想沮榜,設(shè)計(jì)代碼如下
public class MainClass {
public static void main(String[] args) {
try {
//定義 線程池盘榨、隊(duì)列、門閂
ExecutorService service = Executors.newCachedThreadPool();
BlockingQueue<JSONObject> queue = new LinkedBlockingQueue<JSONObject>(100);
CountDownLatch latch = new CountDownLatch(10);
//new 10個(gè)生產(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對(duì)象较曼,無限的遞歸
通過jvisualvm進(jìn)行監(jiān)控,如下圖
可以看到內(nèi)存會(huì)周期性的進(jìn)行回收并保持良好狀態(tài)振愿,這個(gè)猜想也不正確捷犹。
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[]
可以查看它的組成一級(jí)一級(jí)的跟下去會(huì)發(fā)現(xiàn)有elasticsearch——client的影子
最后
解決方法很簡單:線程結(jié)束時(shí),關(guān)閉該線程使用的client客戶端
elasticServer.client.close();
System.out.println("consumer end!");
latch.countDown();
我們要注意的就是在數(shù)據(jù)庫連接的處理上要額外注意档桃,一般情況下不會(huì)出問題枪孩,在頻繁的連接釋放和遞歸時(shí),很有可能引起內(nèi)存泄漏。