內存泄露排查之線程泄露

若是隻關心具體過程,可直接迴歸正途的處理邏輯
原文連接:http://www.javashuo.com/article/p-plzezhmt-cv.htmlhtml

基礎

內存泄露(Memory Leak)

  1. java中內存都是由jvm管理,垃圾回收由gc負責,因此通常狀況下不會出現內存泄露問題,因此容易被你們忽略。
  2. 內存泄漏是指無用對象(再也不使用的對象)持續佔有內存或無用對象的內存得不到及時釋放,從而形成內存空間的浪費稱爲內存泄漏。內存泄露有時不嚴重且不易察覺,這樣開發者就不知道存在內存泄露,須要自主觀察,比較嚴重的時候,沒有內存能夠分配,直接oom。
  3. 主要和溢出作區分。

內存泄露現象

  • heap或者perm/metaspace區不斷增加, 沒有降低趨勢, 最後不斷觸發FullGC, 甚至crash.
  • 若是低頻應用,可能不易發現,可是最終狀況仍是和上述描述一致,內存一致增加

perm/metaspace泄露

  • 這裏存放class,method相關對象,以及運行時常量對象. 若是一個應用加載了大量的class, 那麼Perm區存儲的信息通常會比較大.另外大量的intern String對象也會致使該區不斷增加。
  • 比較常見的一個是Groovy動態編譯class形成泄露。這裏就不展開了

heap泄露

比較常見的內存泄露
  1. 靜態集合類引發內存泄露
  2. 監聽器:但每每在釋放對象的時候卻沒有記住去刪除這些監聽器,從而增長了內存泄漏的機會。
  3. 各類鏈接,數據庫、網絡、IO等
  4. 內部類和外部模塊等的引用:內部類的引用是比較容易遺忘的一種,並且一旦沒釋放可能致使一系列的後繼類對象沒有釋放。非靜態內部類的對象會隱式強引用其外圍對象,因此在內部類未釋放時,外圍對象也不會被釋放,從而形成內存泄漏
  5. 單例模式:不正確使用單例模式是引發內存泄露的一個常見問題,單例對象在被初始化後將在JVM的整個生命週期中存在(以靜態變量的方式),若是單例對象持有外部對象的引用,那麼這個外部對象將不能被jvm正常回收,致使內存泄露
  6. 其它第三方類

本例(線程泄露)

本例現象

  1. 內存佔用率達80%+左右,而且持續上漲,最高點到94%
    內存佔用java

  2. yongGC比較頻繁,在內存比較高的時候,伴有FullGC
    gc次數算法

  3. 線程個個數比較多,最高點達到2w+(這個比較重要,惋惜是後面纔去關注這點)
    線程數據庫

  4. 日誌伴有大量異常,主要是三類
    • fastJosn error
      fastJson錯誤.安全

    • 調用翻譯接口識別語種服務錯誤
      翻譯服務網絡

      翻譯錯誤代碼

    • 對接算法提供的二方包請求錯誤
      predict錯誤多線程

      算法調用錯誤

剛開始走的錯誤彎路

  1. 剛開始發現機器內存佔用比較多,超過80%+,這個時候思考和內存相關的邏輯
  2. 這個時候並無去觀察線程數量,根據現象 一、二、4,、這個過程沒有發現現象3,排查無果後,從新定位問題發現現象3
  3. 因爲現象4中的錯誤日誌比較多,加上內存佔用高,產生了以下想法(因爲本例中不少服務經過mq消費開始)
    • 現象4中的錯誤致使mq重試隊列任務增長,積壓的消息致使mq消費隊列任務增長,最終致使內存上升
    • 因爲異常,邏輯代碼中的異常重試線程池中的任務增長,最終致使任務隊列的長度一直增長,致使內存上升

解決彎路中的疑惑

  • 定位異常
    • fastJson解析異常,光看錯誤會以爲踩到了fastJson的bug(fastJson在以前的版本中,寫入Long類型到Map中,在解析的時候默認是用Int解析器解析,致使溢出錯誤。可是這個bug在後面的版本修復了,目前即便是放入Long類型,若是小於int極限值,默認是int解析,超過int極限,默認long。類中的變量爲Long。直接parse,直接爲Long類型),可是業務代碼中使用的是類直接parse,發現二方包中的類使用了int,可是消息值有的超過int值
    • eas算法鏈路調用錯誤,以前就有(404),可是沒有定位到具體緣由,有知道的望指點下,這裏用try catch作了處理
    • 翻譯服務異常,這裏沒定位到具體緣由,重啓應用後恢復,這裏忘記了作try catch,看來依賴外部服務須要所有try下
  • 確認是不是業務邏輯中錯誤重試隊列問題
    • 否,和業務相關纔會走入重試流程,還在後面
  • 確認是不是Mq消息隊列積壓,以及Mq重試隊列消息積壓致使,確認是不是線程自動調整(metaq/rocketmq)
    • 否,Mq作了消費隊列安全保護
    • consumer異步拉取broker中的消息,processQueue中消息過多就會控制拉取的速率。對於併發的處理場景, 存在三種控制的策略:
      1. queue中的個數是否超過1000
      2. 估算msg佔用的內存大小是否超過100MB
      3. queue中仍然存在的msg(多半是消費失敗的,且回饋broker失敗的)的offset的間隔,過大可能表示會有更多的重複,默認最大間隔是2000。
    • 流控源碼類:com.alibaba.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl#pullMessage,圈中的變量在默認的類中都有初始值
      流控源碼
  • metaq也會本身作動態線程調整,理論上當線程不夠用時,增長線程,adjustThreadPoolNumsThreshold默認值10w,當線程比較多時,減小線程,可是代碼被註釋了,理論上應該沒有自動調整過程,因此這裏也不會由於任務過多增長過多線程
    • 在start啓動的時候,啓動了一批定時任務
      mqStart併發

    • 定時任務中啓動了調整線程的定時任務
      啓動定時調整異步

    • 啓動調整任務
      調整
      調整具體代碼.jvm

迴歸正途的處理邏輯

  • 通過上述分析,發現並非由於異常致使的任務隊列增長過多致使,這個時候,發現了現象3,活動線程數明顯過多,確定是線程泄露,gc不能回收,致使內存一直在增加,因此到這裏,基本上就已經確認是問題由什麼致使,接下來要作的就是確認是這個緣由致使,以及定位到具體的代碼塊
  • 若是沒有具體的監控,通常就是看機器內存情況,cpu,以及jvm的heap,gc,有明顯線程情況的,可jstack相關線程等,最終依然沒法定位到具體代碼塊的能夠dump後分析
登陸涉事機器
  • top,觀察內存佔用率(這裏圖是重啓以後一段時間的)可是cpu佔用率比較高,很快就降下去了,這裏耽誤了一下時間,top -Hp pid,確認那個線程佔用率高,jstack看了下對應的線程在做甚
    top

  • 確認線程是否指定大小,未發現指定,使用的默認值大小

    gc參數

  • 查看heap,gc情況
  • 查看線程情況,可jstack線程,發現線程較多,也能定位到,可是爲了方便,遂dump一份數據詳細觀察堆棧
    • 線程個數
      • cat /proc/{pid}/status (線程數居然這麼多)
        命令行線程個數

      • 因爲線程數比較多,而依然能夠建立,查看Linux普通用戶所容許建立的進程數,使用命令:cat /etc/security/limits.d/90-nproc.conf ,值比較到,遠超當前的個數

    • 線程信息
      線程個數

    • 線程狀態
      線程狀態

    • 定位到問題線程
      • AbstractMultiworkerIOReactor ==》 httpAsycClient ==》如圖所示不能直接定位到代碼塊,因此maven定位引用jar的服務 ==> 具體二方包
    • 若是每次都new線程而不結束,gc中線程是root節點,若是線程沒有結束,不會被回收,因此若是建立大量運行的線程,會致使內存佔用量上升,可是線上到底能建立多少線程呢?

    • 問題代碼塊
      • 方法開始(每次都初始化一個新的客戶端,底層封裝使用httpAsyncClient,httpAsyncClient使用NIO模型,初始化包含一個boss,10個work線程)
        方法開始

      • 方法結束(方法結束都調用了shutdow)
        方法結束

    • 根據現象和對應線程堆棧信息,能肯定線程就是在這邊溢出,客戶端的shutDown方法關閉線程池失效,致使因爲初始的線程都是NIO模式,沒有被結束,因此線程一直積壓增長,可修改成單例模式,限制系統使用一個線程池,上線後解決問題

httpAsyncClient 部分源碼解析
  • 啓動
    • 常駐線程
      • Reactor Thread 負責 connect
      • Worker Thread 負責 read write
    • http啓動線程
    • 線程池命名,也就是上面出現pool--thread-的線程
      普通線程池命名
    • ioEventDispatch 線程
      • 啓動
        啓動
      • worker線程
        worker線程
      • worker線程名稱
        worker線程名稱
      • IO worker運行詳細
      • worker線程實現


  • shutdown 這裏就不作分析了,調用後,線程都會跳出死循環,結束線程,關閉連接等好多清理動做
疑問
  • 雖然每次方法調用都是new新的客戶端,可是結束finally中都調用了shutDown,爲什麼會關閉失敗,上面使用單例模式,只是掩蓋了爲何每次new客戶端而後shutdown失效的緣由
  • httpAsyncClient客戶端在請求失敗的狀況下,httpclient.close()此處會致使主線程阻塞,經源碼發現close方法內部,在線程鏈接池關閉之後, httpAsyncClient對應線程還處於運行之中,一直阻塞在epollWait,詳見上面的線程狀態,這裏目前沒有肯定下爲何調用shutdown以後線程關閉失敗,也沒有任何異常日誌,可是這是致使線程泄露的主要緣由
  • 在本地測試shutdown方法可正常關閉,非常奇怪。若是各位有知道具體的緣由的,望指教
  • 原文連接
相關文章
相關標籤/搜索