內(nèi)存泄露排查之線程泄露

基礎(chǔ)

內(nèi)存泄露(Memory Leak)

java中內(nèi)存都是由jvm管理,垃圾回收由gc負責(zé),所以一般情況下不會出現(xiàn)內(nèi)存泄露問題,所以容易被大家忽略。

內(nèi)存泄漏是指無用對象(不再使用的對象)持續(xù)占有內(nèi)存或無用對象的內(nèi)存得不到及時釋放,從而造成內(nèi)存空間的浪費稱為內(nèi)存泄漏。內(nèi)存泄露有時不嚴(yán)重且不易察覺,這樣開發(fā)者就不知道存在內(nèi)存泄露,需要自主觀察,比較嚴(yán)重的時候,沒有內(nèi)存可以分配,直接oom。

主要和溢出做區(qū)分。

內(nèi)存泄露現(xiàn)象

heap或者perm/metaspace區(qū)不斷增長, 沒有下降趨勢, 最后不斷觸發(fā)FullGC, 甚至crash.

如果低頻應(yīng)用,可能不易發(fā)現(xiàn),但是最終情況還是和上述描述一致,內(nèi)存一致增長

perm/metaspace泄露

這里存放class,method相關(guān)對象,以及運行時常量對象. 如果一個應(yīng)用加載了大量的class, 那么Perm區(qū)存儲的信息一般會比較大.另外大量的intern String對象也會導(dǎo)致該區(qū)不斷增長。

比較常見的一個是Groovy動態(tài)編譯class造成泄露。這里就不展開了

heap泄露

比較常見的內(nèi)存泄露

靜態(tài)集合類引起內(nèi)存泄露

監(jiān)聽器:但往往在釋放對象的時候卻沒有記住去刪除這些監(jiān)聽器,從而增加了內(nèi)存泄漏的機會。

各種連接,數(shù)據(jù)庫、網(wǎng)絡(luò)、IO等

內(nèi)部類和外部模塊等的引用:內(nèi)部類的引用是比較容易遺忘的一種,而且一旦沒釋放可能導(dǎo)致一系列的后繼類對象沒有釋放。非靜態(tài)內(nèi)部類的對象會隱式強引用其外圍對象,所以在內(nèi)部類未釋放時,外圍對象也不會被釋放,從而造成內(nèi)存泄漏

單例模式:不正確使用單例模式是引起內(nèi)存泄露的一個常見問題,單例對象在被初始化后將在JVM的整個生命周期中存在(以靜態(tài)變量的方式),如果單例對象持有外部對象的引用,那么這個外部對象將不能被jvm正?;厥眨瑢?dǎo)致內(nèi)存泄露

其它第三方類

本例(線程泄露)

本例現(xiàn)象

內(nèi)存占用率達80%+左右,并且持續(xù)上漲,最高點到94%

yongGC比較頻繁,在內(nèi)存比較高的時候,伴有FullGC

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

日志伴有大量異常,主要是三類

fastJosn error

調(diào)用翻譯接口識別語種服務(wù)錯誤

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

剛開始走的錯誤彎路

剛開始發(fā)現(xiàn)機器內(nèi)存占用比較多,超過80%+,這個時候思考和內(nèi)存相關(guān)的邏輯

這個時候并沒有去觀察線程數(shù)量,根據(jù)現(xiàn)象 1、2、4,、這個過程沒有發(fā)現(xiàn)現(xiàn)象3,排查無果后,重新定位問題發(fā)現(xiàn)現(xiàn)象3

由于現(xiàn)象4中的錯誤日志比較多,加上內(nèi)存占用高,產(chǎn)生了如下想法(由于本例中很多服務(wù)通過mq消費開始)

現(xiàn)象4中的錯誤導(dǎo)致mq重試隊列任務(wù)增加,積壓的消息導(dǎo)致mq消費隊列任務(wù)增加,最終導(dǎo)致內(nèi)存上升

由于異常,邏輯代碼中的異常重試線程池中的任務(wù)增加,最終導(dǎo)致任務(wù)隊列的長度一直增加,導(dǎo)致內(nèi)存上升

解決彎路中的疑惑

定位異常

fastJson解析異常,光看錯誤會覺得踩到了fastJson的bug(fastJson在之前的版本中,寫入Long類型到Map中,在解析的時候默認(rèn)是用Int解析器解析,導(dǎo)致溢出錯誤。但是這個bug在后面的版本修復(fù)了,目前即使是放入Long類型,如果小于int極限值,默認(rèn)是int解析,超過int極限,默認(rèn)long。類中的變量為Long。直接parse,直接為Long類型),但是業(yè)務(wù)代碼中使用的是類直接parse,發(fā)現(xiàn)二方包中的類使用了int,但是消息值有的超過int值

eas算法鏈路調(diào)用錯誤,之前就有(404),但是沒有定位到具體原因,有知道的望指點下,這里用try catch做了處理

翻譯服務(wù)異常,這里沒定位到具體原因,重啟應(yīng)用后恢復(fù),這里忘記了做try catch,看來依賴外部服務(wù)需要全部try下

確認(rèn)是否是業(yè)務(wù)邏輯中錯誤重試隊列問題

否,和業(yè)務(wù)相關(guān)才會走入重試流程,還在后面

確認(rèn)是否是Mq消息隊列本以及Mq重試隊列 消息積壓導(dǎo)致

否,Mq做了消費隊列安全保護

consumer異步拉取broker中的消息,processQueue中消息過多就會控制拉取的速率。對于并發(fā)的處理場景, 存在三種控制的策略:

1. queue中的個數(shù)是否超過1000

2. 估算msg占用的內(nèi)存大小是否超過100MB

3. queue中仍然存在的msg(多半是消費失敗的,且回饋broker失敗的)的offset的間隔,過大可能表示會有更多的重復(fù),默認(rèn)最大間隔是2000。

流控源碼類:com.alibaba.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl#pullMessage,圈中的變量在默認(rèn)的類中都有初始值

metaq也會自己做動態(tài)線程調(diào)整,理論上當(dāng)線程不夠用時,增加線程,adjustThreadPoolNumsThreshold默認(rèn)10w,當(dāng)線程比較多時,減少線程,但是代碼被注釋了,理論上應(yīng)該沒有自動調(diào)整過程,所以這里也不會因為任務(wù)過多增加過多線程數(shù)

在start啟動的時候,啟動了一批定時任務(wù)

定時任務(wù)中啟動了調(diào)整線程的定時任務(wù)

啟動調(diào)整任務(wù)

回歸正途的處理邏輯

經(jīng)過上述分析,發(fā)現(xiàn)并不是因為異常導(dǎo)致的任務(wù)隊列增加過大導(dǎo)致,這個時候,發(fā)現(xiàn)了現(xiàn)象3,活動線程數(shù)明顯過多,肯定是線程泄露,gc不能回收,導(dǎo)致內(nèi)存一直在增長,所以到這里,基本上就已經(jīng)確認(rèn)是問題由什么導(dǎo)致,接下來要做的就是確認(rèn)是這個原因?qū)е?,以及定位到具體的代碼塊

如果沒有具體的監(jiān)控,一般就是看內(nèi)存,cpu,heap狀況,gc狀況等,最終依然無法定位到代碼塊的可以dump

登錄涉事機器

top,觀察內(nèi)存占用率(這里圖是重啟之后一段時間的)但是cpu占用率比較高,很快就降下去了,這里耽誤了一下時間,top -Hp pid,確認(rèn)那個線程占用率高,jstack看了下對應(yīng)的線程在作甚

確認(rèn)線程是否指定大小,未發(fā)現(xiàn)指定,使用的默認(rèn)值

查看heap,gc狀況

查看線程狀況,可jstack線程,發(fā)現(xiàn)線程較多,也能定位到,但是為了方便,遂dump一份數(shù)據(jù)詳細觀察堆棧

線程個數(shù)

cat /proc/{pid}/status (線程數(shù)竟然這么多)

由于線程數(shù)比較多,而依然可以創(chuàng)建,查看Linux普通用戶所允許創(chuàng)建的進程數(shù),使用命令:cat /etc/security/limits.d/90-nproc.conf ,值比較到,遠超當(dāng)前的個數(shù)

線程信息

線程狀態(tài)

定位到問題線程

AbstractMultiworkerIOReactor ==》 httpAsycClient ==》如圖所示不能直接定位到代碼塊,所以maven定位引用jar的服務(wù) ==> 具體二方包

如果每次都new線程而不結(jié)束,gc中線程是root節(jié)點,如果線程沒有結(jié)束,不會被回收,所以如果創(chuàng)建大量運行的線程,會導(dǎo)致內(nèi)存占用量上升,但是線上到底能創(chuàng)建多少線程呢?

問題代碼塊

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

方法結(jié)束(方法結(jié)束都調(diào)用了shutdow)

根據(jù)現(xiàn)象和對應(yīng)線程堆棧信息,能確定線程就是在這邊溢出,客戶端的shutDown方法關(guān)閉線程池失效,導(dǎo)致由于初始的線程都是NIO模式,沒有被結(jié)束,所以線程一直積壓增加,可修改為單例模式,限制系統(tǒng)使用一個線程池

httpAsyncClient部分源碼

啟動

線程池命名,也就是上面出現(xiàn)pool--thread-的線程

ioEventDispatch 線程

啟動

worker線程

worker線程名稱

IO worker運行詳細

worker線程實現(xiàn)

shutdown 這里就不做分析了,調(diào)用后,線程都會跳出死循環(huán),結(jié)束線程,關(guān)閉鏈接等好多清理動作

疑問

雖然每次方法調(diào)用都是new新的客戶端,但是結(jié)束finally中都調(diào)用了shutDown,為何會關(guān)閉失敗,上面使用單例模式,只是掩蓋了為什么每次new客戶端然后shutdown失效的原因

httpAsyncClient客戶端在請求失敗的情況下,httpclient.close()此處會導(dǎo)致主線程阻塞;經(jīng)源碼發(fā)現(xiàn)close 方法內(nèi)部,在線程連接池關(guān)閉以后, httpclient對應(yīng)線程還處于運行之中,一直阻塞在epollWait,詳見上面的線程狀態(tài),這里目前沒有確定下為什么調(diào)用shutdown之后線程關(guān)閉失敗,也沒有任何異常日志,但是這是導(dǎo)致線程泄露的主要原因

在本地測試shutdown方法可正常關(guān)閉,很是奇怪。如果各位有知道具體的原因的,望指教

歡迎工作一到五年的Java工程師朋友們加入Java程序員開發(fā): 721575865

群內(nèi)提供免費的Java架構(gòu)學(xué)習(xí)資料(里面有高可用、高并發(fā)、高性能及分布式、Jvm性能調(diào)優(yōu)、Spring源碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構(gòu)資料)合理利用自己每一分每一秒的時間來學(xué)習(xí)提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!

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

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

  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對...
    cosWriter閱讀 11,650評論 1 32
  • 線程池ThreadPoolExecutor corepoolsize:核心池的大小,默認(rèn)情況下,在創(chuàng)建了線程池之后...
    irckwk1閱讀 864評論 0 0
  • 第一部分 來看一下線程池的框架圖,如下: 1、Executor任務(wù)提交接口與Executors工具類 Execut...
    壓抑的內(nèi)心閱讀 4,392評論 1 24
  • 在一個方法內(nèi)部定義的變量都存儲在棧中,當(dāng)這個函數(shù)運行結(jié)束后,其對應(yīng)的棧就會被回收,此時,在其方法體中定義的變量將不...
    Y了個J閱讀 4,570評論 1 14
  • 這是平淡的一年,我跨過了40歲的大關(guān)。這是希望的一年,我終于看到夢寐以求的大海。這是充滿驚喜的一年,妹妹為家里又添...
    路語旁集閱讀 201評論 0 0

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