內存泄露排查之線程泄露


如果只關心具體過程,可直接回歸正途的處理邏輯
原文鏈接:https://www.cnblogs.com/guozp/p/10597327.html

基礎

內存泄露(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%
    內存占用

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

  3. 線程個個數比較多,最高點達到2w+(這個比較重要,可惜是后面才去關注這點)
    線程

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

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

      翻譯錯誤代碼

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

      算法調用錯誤

剛開始走的錯誤彎路

  1. 剛開始發現機器內存占用比較多,超過80%+,這個時候思考和內存相關的邏輯
  2. 這個時候並沒有去觀察線程數量,根據現象 1、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重試隊列 消息積壓導致
    • 否,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

    • 定時任務中啟動了調整線程的定時任務
      啟動定時調整

    • 啟動調整任務
      調整
      調整具體代碼.

回歸正途的處理邏輯

  • 經過上述分析,發現並不是因為異常導致的任務隊列增加過大導致,這個時候,發現了現象3,活動線程數明顯過多,肯定是線程泄露,gc不能回收,導致內存一直在增長,所以到這里,基本上就已經確認是問題由什么導致,接下來要做的就是確認是這個原因導致,以及定位到具體的代碼塊
  • 如果沒有具體的監控,一般就是看內存,cpu,heap狀況,gc狀況等,最終依然無法定位到代碼塊的可以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部分源碼
  • 啟動
    http啟動線程
    • 線程池命名,也就是上面出現pool--thread-的線程
      普通線程池命名
    • ioEventDispatch 線程
      • 啟動
        啟動
      • worker線程
        worker線程
      • worker線程名稱
        worker線程名稱
      • IO worker運行詳細
      • worker線程實現


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

注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
  © 2014-2022 ITdaan.com 联系我们: