一次線上內(nèi)存泄漏的解決過程

內(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)行即可

image.png

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

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

分析程序崩潰時堆文件

程序運(yùn)行時,設(shè)置參數(shù)

-Xms200m
-Xmx200m
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=d:/dump/

設(shè)置最大內(nèi)存和指定OutOfMemoryError時存儲堆文件的位置

我們使用jvisualvm打開堆文件java_pid42132.hprof


image.png

占用內(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)控,如下圖


QQ圖片20180918152927.png

可以看到內(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

image.png

雙擊打開java.lang.Object[]可以查看它的組成
image.png

一級一級的跟下去會發(fā)現(xiàn)有elasticsearch——client的影子

最后

解決方法很簡單:線程結(jié)束時,關(guān)閉該線程使用的client客戶端

elasticServer.client.close();
System.out.println("consumer end!");
latch.countDown();

我們要注意的就是在數(shù)據(jù)庫連接的處理上要額外注意,一般情況下不會出問題,在頻繁的連接釋放和遞歸時,很有可能引起內(nèi)存泄漏。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 本文是我自己在秋招復(fù)習(xí)時的讀書筆記,整理的知識點(diǎn),也是為了防止忘記,尊重勞動成果,轉(zhuǎn)載注明出處哦!如果你也喜歡,那...
    波波波先森閱讀 11,621評論 4 56
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,578評論 19 139
  • 廣袤無垠的撒哈拉有多少粒沙在風(fēng)中掙扎翻滾,平凡的世界里就有多少只靈魂在塵世間愛恨情仇。佛說:阿彌佗佛,一切皆是因緣~
    清輝V閱讀 391評論 2 1
  • 天亮了,太陽升起,門外傳來遠(yuǎn)遠(yuǎn)的犬吠聲。 睜開眼,伸個懶腰,披上外套,飲下一杯冽的山泉水,和著一塊小餅。晨...
    聽歌的古董閱讀 297評論 0 0
  • 0918(day2) 書名:《贊賞的5種語言》 正文字?jǐn)?shù):720字 1.《學(xué)會傾聽,讓溝通更順暢》 里克.里德是一...
    蘋果Apple來了閱讀 285評論 0 0

友情鏈接更多精彩內(nèi)容