Tomcat關閉後,應用進程無法退出的解決方案

前奏: 最近在Linux部署tomcat應用的時候發現停止tomcat後,自己的進程還在,必須要用kill命令強行殺掉進程,每次這樣做感覺很不妥,所以現在我來找一下出現這個問題的根本原因並給出解決方案。


背景:有一天我發現公司的Ubantu服務器內存不夠用了……,32G內存!而且這臺服務器只部署了我一個服務。what?神馬情況,一臉萌幣。讓我靜靜…………。

好了,安靜了,現在開始分析原因。

首先我執行了 ps|aux grep tomat(意思就是查找所有帶有tomcat相關的進程),發現有N個tomcat進程在運行(截圖省略),每個進程佔用1個G內存左右。關鍵還調用不到它……,很憂桑。仔細想了想難道是以前的tomcat都沒關閉成功? ./shutdown這個方法失靈了?

是的,的確失靈了,現在我們來分析一下它失靈的原因。

第一步.首先打開tomcat的logs裏面catalina.out查看日誌,發現出現如下日誌

這裏寫圖片描述
針對圖中的兩處紅圈做以下分析:
a.從1處可以看出大致意思是說tomcat嘗試關閉一個名爲pool-1-thread-4的線程但是失敗了,這樣很有可能會導致內存泄漏。一聽到內存泄露,大家可能會擔心受怕,不過沒關係,我慢慢來找出原因並解決它。
b.從2處可以看到這裏我用了一個第三方分詞服務的工具:word分詞1.3版。很有可能導致進程無法停止的罪魁禍首是它導致的。

第二步.於是我根據它給的代碼提示,找到了分詞工具中的源碼代碼塊。
這裏寫圖片描述
從這裏我們能看到它的服務裏面有一個資源監控的方法,這方法運行在ExceutorService線程池裏面,它的作用是爲了監聽目錄下資源的變化,如果有變動,則重新加載資源,和web.xml配置log4j的配置文件變化監聽很像。而且它使用的是while(true)死循環並且沒有任何判斷break操作的地方,同時根據WatchService.take我們發現它在這裏進行了線程等待,每當資源變化的時候,WatchService就能take到值並返回,從而進行下一次等待。如果它運行在一個守護線程裏面,當主線程關閉的時候,它會進行自動銷燬。但是它是線程池開闢出來的,爲非守護線程。所以如果要結束這個線程必須調用ExceutorService的shutdown方法,但是這個方法執行的前提是等待線程池裏面所有的線程都完成自己的任務纔會調用,很明顯在我們這個死循環的代碼塊裏面它的任務永遠都不會完成。所以我們需要強制結束,調用它的shotdownNow方法。可能在它的第三方工具裏面它的線程池對象是私有的,並且沒有給出強制結束的方法,所以只好另闢蹊徑了。

第三步.我們利用jstack堆棧跟蹤工具追蹤驗證一下我剛剛的分析。
在linux服務器中查看我的web服務的java進程的pid,比如是123,然後輸入 jstack 123。

這裏寫圖片描述
在這裏我們找了到我們剛剛的日誌裏面的pool-1-thread-4線程,從命名上帶有pool我們就能看出它是從線程池裏面開闢出來的,而且它並沒有daemon標誌,說明它是一個非守護線程,從waiting on condition可以看出它處於一個線程等待的狀態,從之前的watchService.take來看,它是在等待資源的變化。經過以上分析,已經很明確進程沒能成功關閉是由於它一直處於線程等待導致的,jvm會認爲你這個線程裏面的任務並沒有執行完,從而關閉失敗。

到這裏,我們已經找到了關閉失敗的原因了,現在我們來看一下如何解決這個問題。

剛剛提到分詞工具裏面的線程池對象已經被設爲私有,並且並沒有提供較好的關閉方法。所以我這裏找了兩個方案來結束這個進程。

方案一
在程序shutdown前,設置監聽,停止之前我們需要做一些事情,就是關閉這些進程。我們可以獲取到所有的jvm進程並執行中斷操作。
a.a.獲取jvm的所有進程。

public static Thread[] findAllThreads() {  
        ThreadGroup group =   
            Thread.currentThread().getThreadGroup();  
        ThreadGroup topGroup = group;  
        // traverse the ThreadGroup tree to the top  
        while ( group != null ) {  
            topGroup = group;  
            group = group.getParent();  
        }  
        // Create a destination array that is about  
        // twice as big as needed to be very confident  
        // that none are clipped.  
        int estimatedSize = topGroup.activeCount() * 2;  
        Thread[] slackList = new Thread[estimatedSize];  
        // Load the thread references into the oversized  
        // array. The actual number of threads loaded   
        // is returned.  
        int actualSize = topGroup.enumerate(slackList);  
        // copy into a list that is the exact size  
        Thread[] list = new Thread[actualSize];  
        System.arraycopy(slackList, 0, list, 0, actualSize);  
        return list;  
    }

a.b對每個線程進行中斷操作。

        Thread[] threads = findAllThreads();
        for(Thread thread : threads){
            thread.interrupt();
        }

至此,在執行tomcat ./shutdown後就可以順利地關閉進程。

方案二
還有個更暴力的方法結束進程,就是在結束的時候執行

System.exit(0);

可以直接將進程結束,這裏你可能會擔心,這個方法是結束jvm虛擬機,調用這個方法會不會把我linux裏面其它的java進程全部結束,這裏你的擔心是多餘的,如果你對jvm的運行機制有一定的瞭解,就可以大不必擔心這個了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章