1. 程式人生 > >Java執行緒洩露的分析與處理

Java執行緒洩露的分析與處理

1. 生產環境的異常現象及初步分析

最近發現系統程式記憶體消耗越來越大,開始並沒特別注意,就簡單調了一下jvm引數。但直到前些天記憶體爆滿,持續Full GC,這肯定出現了記憶體洩露。

原以為哪裡出現了比較低階的錯誤,所以很直接想到先去看看程式是在跑哪段程式碼。jstack -l <pid>以後,居然有上千個執行緒,而且都是屬於RUNNING並WAIT的狀態。

I/O dispatcher 125" #739 prio=5 os_prio=0 tid=0x0000000002394800 nid=0x1e2a runnable [0x00007f5c2125b000]
   java.lang.Thread.State: RUNNABLE
        at sun.nio.ch.EPollArrayWrapper.epollWait(Native Method)
        at sun.nio.ch.EPollArrayWrapper.poll(EPollArrayWrapper.java:269)
        at sun.nio.ch.EPollSelectorImpl.doSelect(EPollSelectorImpl.java:79)
        at sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:86)
        - locked <0x00000007273401d0> (a sun.nio.ch.Util$2)
        - locked <0x00000007273401c0> (a java.util.Collections$UnmodifiableSet)
        - locked <0x00000007273401e0> (a sun.nio.ch.EPollSelectorImpl)
        at sun.nio.ch.SelectorImpl.select(SelectorImpl.java:97)
        at org.apache.http.impl.nio.reactor.AbstractIOReactor.execute(AbstractIOReactor.java:257)
        at org.apache.http.impl.nio.reactor.BaseIOReactor.execute(BaseIOReactor.java:106)
        at org.apache.http.impl.nio.reactor.AbstractMultiworkerIOReactor$Worker.run(AbstractMultiworkerIOReactor.java:590)
        at java.lang.Thread.run(Thread.java:745)

   Locked ownable synchronizers:
        - None

"pool-224-thread-1" #738 prio=5 os_prio=0 tid=0x00007f5c463f4000 nid=0x1e29 runnable [0x00007f5c2024b000]
   java.lang.Thread.State: RUNNABLE
        at sun.nio.ch.EPollArrayWrapper.epollWait(Native Method)
        at sun.nio.ch.EPollArrayWrapper.poll(EPollArrayWrapper.java:269)
        at sun.nio.ch.EPollSelectorImpl.doSelect(EPollSelectorImpl.java:79)
        at sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:86)
        - locked <0x0000000727340478> (a sun.nio.ch.Util$2)
        - locked <0x0000000727340468> (a java.util.Collections$UnmodifiableSet)
        - locked <0x0000000727340488> (a sun.nio.ch.EPollSelectorImpl)
        at sun.nio.ch.SelectorImpl.select(SelectorImpl.java:97)
        at org.apache.http.impl.nio.reactor.AbstractMultiworkerIOReactor.execute(AbstractMultiworkerIOReactor.java:342)
        at org.apache.http.impl.nio.conn.PoolingNHttpClientConnectionManager.execute(PoolingNHttpClientConnectionManager.java:191)
        at org.apache.http.impl.nio.client.CloseableHttpAsyncClientBase$1.run(CloseableHttpAsyncClientBase.java:64)
        at java.lang.Thread.run(Thread.java:745)

   Locked ownable synchronizers:
        - None

我以下的思考路徑都未能解決(自己記錄一下,看官可以跳過...)

  1. 檢視執行緒的stack,看呼叫處是否有問題。這個一般都能解決問題,但是上面的異常執行緒棧確實沒什麼資訊量,無法定位。

  2. Google了一下有關大量這個執行緒停在epollwait的資料,發現這個現象和epoll nio的bug是一樣的,還以為碰到了一個無法處理的高階問題。第一反應就是去HttpClient的官網查bug日誌,結果還真發現了最近的升級有解決類似問題的,然後升級到最新版問題依舊。但是最後仔細想想,也確實不太可能,畢竟應用場景還是比較普通的。

  3. jmap -histo <pid>看了一下物件,結果發現存在InternalHttpAsyncClient數量和洩露的執行緒數量剛好相等,所以基本就確定是這個物件的建立和回收有問題。但是這是誰建立的?

  4. 查了呼叫棧和異常物件的package,發現是HttpClient的,把本地所有相關呼叫都查了一遍,看起來寫的也都是對的。

  5. 搬出jvirtualvm的效能分析工具,發現只能看到洩露現象,無法定位問題。

這下懵逼了,剛好忙其他事,就放了幾天順帶考慮一下,還好洩露比較慢,問題處理不著急。。。

2. 執行緒洩露的分析方法

處理這個問題的關鍵:必須準確知道是什麼洩露了執行緒!

在Google過程中突然受到啟發,JDK中的工具是應該可以分析引用的。最後發現jhat - Java Heap Analysis Tool正是我要的。

最終解決方式:

  1. jmap -F -dump:format=b,file=tomcat.bin <pid>

     匯出tomcat的記憶體

  2. jhat -J-Xmx4g <heap dump file> 分析Heap中的資訊(注意:分析非常消耗CPU和記憶體,儘量在配置較好的機器上執行)

  3. 檢視相關物件的reference,OQL也可以用,但是網頁版直接點連結也夠用了。

3. 鎖定原因並解決

從之前異常heap中發現存在的問題物件有如下這些:

$ cat histo | grep org.apache.http. | grep 1944 | less 
 197:          1944         217728  org.apache.http.impl.nio.conn.ManagedNHttpClientConnectionImpl 232:          1944         171072  org.apache.http.impl.nio.conn.CPool 233:          1944         171072  org.apache.http.impl.nio.reactor.DefaultConnectingIOReactor 248:          1944         155520  org.apache.http.impl.nio.reactor.BaseIOReactor 249:          1944         155520  org.apache.http.impl.nio.reactor.IOSessionImpl 276:          1944         139968  org.apache.http.impl.nio.client.InternalHttpAsyncClient 277:          1944         139968  org.apache.http.impl.nio.conn.CPoolEntry 323:          1944         108864  org.apache.http.impl.nio.client.MainClientExec 363:          1944          93312  org.apache.http.impl.nio.codecs.DefaultHttpResponseParser 401:          1944          77760  org.apache.http.impl.nio.reactor.SessionInputBufferImpl 402:          1944          77760  org.apache.http.impl.nio.reactor.SessionOutputBufferImpl 403:          1944          77760  org.apache.http.nio.protocol.HttpAsyncRequestExecutor$State 442:          1944          62208  org.apache.http.impl.cookie.DefaultCookieSpecProvider 443:          1944          62208  org.apache.http.impl.nio.conn.PoolingNHttpClientConnectionManager 444:          1944          62208  org.apache.http.nio.conn.ssl.SSLIOSessionStrategy 445:          1944          62208  org.apache.http.nio.pool.AbstractNIOConnPool$2
 511:          1944          46656  [Lorg.apache.http.impl.nio.reactor.AbstractMultiworkerIOReactor$Worker; 512:          1944          46656  [Lorg.apache.http.impl.nio.reactor.BaseIOReactor; 513:          1944          46656  org.apache.http.conn.ssl.DefaultHostnameVerifier 514:          1944          46656  org.apache.http.impl.cookie.DefaultCookieSpec 515:          1944          46656  org.apache.http.impl.cookie.NetscapeDraftSpecProvider 516:          1944          46656  org.apache.http.impl.nio.client.CloseableHttpAsyncClientBase$1
 517:          1944          46656  org.apache.http.impl.nio.client.InternalIODispatch 518:          1944          46656  org.apache.http.impl.nio.codecs.DefaultHttpRequestWriter 519:          1944          46656  org.apache.http.impl.nio.conn.PoolingNHttpClientConnectionManager$ConfigData 520:          1944          46656  org.apache.http.impl.nio.conn.PoolingNHttpClientConnectionManager$InternalAddre***esolver 521:          1944          46656  org.apache.http.impl.nio.conn.PoolingNHttpClientConnectionManager$InternalConnectionFactory 522:          1944          46656  org.apache.http.impl.nio.reactor.AbstractMultiworkerIOReactor$Worker 523:          1944          46656  org.apache.http.nio.protocol.HttpAsyncRequestExecutor 603:          1944          31104  org.apache.http.client.protocol.RequestExpectContinue 604:          1944          31104  org.apache.http.conn.routing.BasicRouteDirector 605:          1944          31104  org.apache.http.impl.auth.HttpAuthenticator 606:          1944          31104  org.apache.http.impl.conn.DefaultRoutePlanner 607:          1944          31104  org.apache.http.impl.cookie.IgnoreSpecProvider 608:          1944          31104  org.apache.http.impl.nio.SessionHttpContext 609:          1944          31104  org.apache.http.impl.nio.reactor.AbstractIOReactor$1
 610:          1944          31104  org.apache.http.impl.nio.reactor.AbstractMultiworkerIOReactor$DefaultThreadFactory 611:          1944          31104  org.apache.http.nio.pool.AbstractNIOConnPool$InternalSessionRequestCallback

接下來要找出到底誰new了這些物件,這些異常Object中很多是內部field,所以要先找出最外層的物件。這個就只是邊猜邊看了,結果發現就是InternalHttpAsyncClient。點開進去看了一下,發現有一堆Instance,最後了發現洩露的物件。也可以用OQL select referrers(c) from org.apache.http.impl.nio.client.InternalHttpAsyncClient c

instance of [email protected]38 (128 bytes)

Class:

class org.apache.http.impl.nio.client.InternalHttpAsyncClientInstance data members:

...

References to this object:

[email protected]x932be6c8 (40 bytes) : field this$0
[email protected] (32 bytes) : field httpClient

這裡的資訊就是阿里雲的mqs建立了這些物件。去看了一下程式碼,書寫看似沒有問題,實際上,連線壓根忘記關了。有問題的阿里雲MQS文件是這個,但是最新版本的官網文件已經改用了org.eclipse.jetty.client.HttpClient,也是沒有顯式呼叫stop函式,希望這個類庫不會出現此問題。

@Service
public class AliyunService implements IAliyunService {
    private static Logger logger = Logger.getLogger(AliyunService.class.getName());
    
    @Autowired
    private AliyunConfig aliyunConfig;    
    
    @Override
    public void sendMessage(String content) {
        MQSClient client = new DefaultMQSClient(aliyunConfig.mqEndpoint, aliyunConfig.mqAccessId, aliyunConfig.mqAccessKey);
        String queueName = aliyunConfig.mqQueue;        try {
            CloudQueue queue = client.getQueueRef(queueName);            
            // queue沒做關閉處理,應該最後加上
            // finally{ queue.close(); }
            Message message = new Message();
            message.setMessageBody(content);
            queue.putMessage(message);
        } catch (Exception e) {
            logger.warning(e.getMessage());
        }
    }

}

以下是MQS給的jar中相應關閉的原始碼

public final class CloudQueue {    private ServiceClient serviceClient;
    ...    
    public void close() {        
        if(this.serviceClient != null) {            
            this.serviceClient.close();
        }
    }
    
}

真相大白!至此修改後,問題順利解決。

4. 總結

首先,這個問題的解決確實還是要善用並熟悉JDK工具*,之前對jhat的理解不深,導致第一時間沒有想到這個解決方案。日後再有記憶體問題,會有更犀利的解決方法了。

其次,熟悉了執行緒洩露的現象,解決方式還是去找執行緒的物件,說到底,還是物件的洩露。

1、具有1-5工作經驗的,面對目前流行的技術不知從何下手,

需要突破技術瓶頸的可以加。

2、在公司待久了,過得很安逸,

但跳槽時面試碰壁。

需要在短時間內進修、跳槽拿高薪的可以加。

3、如果沒有工作經驗,但基礎非常紮實,對java工作機制,

常用設計思想,常用java開發框架掌握熟練的,可以加。

4、覺得自己很牛B,一般需求都能搞定。

但是所學的知識點沒有系統化,很難在技術領域繼續突破的可以加。

5. 群號:高階架構群 Java進階群:180705916.備註好資訊!送架構視訊。

6.阿里Java高階大牛直播講解知識點,分享知識,

多年工作經驗的梳理和總結,帶著大家全面、

科學地建立自己的技術體系和技術認知!