前序
在武俠編碼的江湖中溜歪,內(nèi)存泄漏猶如隱秘殺手若专,潛伏于應用程序的各個角落,悄無聲息地吞噬著系統(tǒng)資源蝴猪。若不及時發(fā)現(xiàn)和解決调衰,必將導致內(nèi)存枯竭,應用崩潰自阱。
背景:內(nèi)存泄漏的由來
內(nèi)存泄漏嚎莉,乃程序運行過程中,已不再使用的內(nèi)存塊未被及時回收沛豌,導致內(nèi)存使用量不斷增加的現(xiàn)象趋箩。此問題多發(fā)于對象生命周期管理不當之處赃额,如持有對象引用過長,或未能及時釋放資源叫确,終致內(nèi)存枯竭跳芳,系統(tǒng)崩潰。
在JVM的世界中竹勉,內(nèi)存泄漏常見于以下幾種情況:
- 靜態(tài)集合類:如 HashMap飞盆、ArrayList 等,若不斷向其添加對象而不清理次乓,易造成內(nèi)存泄漏吓歇。
- 長生命周期對象持有短生命周期對象引用:如單例模式中的對象持有臨時對象引用,導致臨時對象無法被垃圾回收票腰。
- 未關閉的資源:如數(shù)據(jù)庫連接城看、文件流等,若未及時關閉丧慈,亦會導致內(nèi)存泄漏析命。
- 監(jiān)聽器與回調(diào)函數(shù):未及時移除的監(jiān)聽器或回調(diào)函數(shù),可能導致對象無法被回收逃默。
解決方案:內(nèi)存泄漏的破解之道
- 善用工具鹃愤,探查隱患
如同俠客需借助兵器,程序員亦需運用內(nèi)存分析工具完域,如 jvisualvm软吐、jmap、jhat 等吟税,探查內(nèi)存使用情況凹耙,定位內(nèi)存泄漏之源。常規(guī)步驟如下:- 使用 jmap 生成堆轉儲文件:jmap -dump:live,format=b,file=heap_dump.hprof <pid>
- 使用 jvisualvm 或 Eclipse MAT 分析堆轉儲文件肠仪,查找無法回收的對象肖抱。
- 優(yōu)化代碼,清理內(nèi)存
針對發(fā)現(xiàn)的內(nèi)存泄漏問題异旧,需優(yōu)化代碼意述,確保對象在不再使用時盡快釋放。具體方法如下:- 及時清理集合:對于使用完畢的對象吮蛹,及時從集合中移除荤崇。
- 合理管理對象引用:避免長生命周期對象持有短生命周期對象引用,可使用弱引用(WeakReference)來管理潮针。
- 關閉資源:對于文件流术荤、數(shù)據(jù)庫連接等資源,在使用完畢后每篷,務必調(diào)用 close() 方法關閉瓣戚。
- 移除監(jiān)聽器:在適當時機端圈,移除不再需要的監(jiān)聽器或回調(diào)函數(shù)。
加強監(jiān)控子库,防患未然
如同江湖俠客需時刻警惕枫笛,程序員亦需持續(xù)監(jiān)控內(nèi)存使用情況,防患于未然刚照⌒糖桑可使用監(jiān)控工具如 Prometheus、Grafana 等无畔,實時查看內(nèi)存使用情況啊楚,及時發(fā)現(xiàn)異常。
滴滴滴浑彰、滴滴滴....,代碼劍宗中的某一處洞府中的告警聲不絕于耳恭理,近眼望去,洞府正中央的蒲團上正坐著一位雙眼緊閉身材健碩的男子郭变,此男子的旁邊還擺放著籃球颜价、杠鈴等道具,從洞府的擺布不難看出此男子平日經(jīng)常鍛煉诉濒,以至于他的身型較于常人更加挺拔高大周伦。男子聽到告警聲睜開雙眼,男子雙眸中充滿精光未荒,看來此次閉關男子有了不少收獲专挪。聽到警告聲的男子眼神中閃過了一絲不耐煩,嘴里輕輕碎了一聲片排,然后不緊不慢地從袖中拿出一個圓盤寨腔,此圓盤此時一直閃爍著紅光,并且一直發(fā)出“滴滴滴”的聲音率寡,男子用手輕撫手中圓盤迫卢,身前映射出一個巨大光影,光影里面有一些畫面跟文字冶共,男子大概花了一刻鐘時間掃描完光影里的內(nèi)容乾蛤。他臉上閃過一絲苦澀,然后說道:“該死的比默,竟然出現(xiàn)了內(nèi)存告警”幻捏,隨即只見男子雙手一揮把光影打散盆犁,男子站了起來朝著旁邊的一個房間走去命咐。
而洞府中的男子就是咱們的男主“阿強”。他之前正坐在蒲團上修煉內(nèi)功到一個關鍵時刻谐岁,不曾想被圓盤的告警打斷醋奠,此時的阿強心情不是很美麗.......
內(nèi)存緊急處理
阿強離開洞府的第一件事情就是通過告警身份牌進入“乾坤內(nèi)存法陣”查看告警的應用陣腳榛臼,阿強查看應用的內(nèi)存情況跟系統(tǒng)的一些指標如下圖所示,阿強看到這個內(nèi)存水位情況就發(fā)現(xiàn)了不對勁窜司。應用從晚上03:00開始到目前為止內(nèi)存一直處于一個緩慢上升狀態(tài)沛善。不過此時阿強倒也沒有因此慌了自己陣腳,阿強根據(jù)以前處理類似問題的經(jīng)驗塞祈,他先通過“乾坤內(nèi)存法陣”中的應用內(nèi)存Dump導出功能先將內(nèi)存快照給dump下來金刁,然后就將應用的容器進行重啟的操作。
實時區(qū)間熱點圖
[圖片上傳失敗...(image-83d0fd-1727321763931)]
實時線程數(shù)
[圖片上傳失敗...(image-adfc78-1727321763931)]
實時gc數(shù)量
[圖片上傳失敗...(image-f145ab-1727321763931)]
實時堆內(nèi)存信息
[圖片上傳失敗...(image-1a11c0-1727321763931)]
容器實時內(nèi)存情況
[圖片上傳失敗...(image-407905-1727321763931)]
不久议薪,應用的快照文件就dump了下來尤蛮,阿強看著dump下來的文件并沒有直接去分析而是優(yōu)先去詢問了負責此應用的人詢問了一下具體情況。2個時辰后斯议,阿強大概從負責此應用的人口中知道了此應用的基本情況产捞。此應用名叫G服務,是從F服務中拆出來的一個應用哼御,拆出來的G服務的代碼內(nèi)容與G服務是保持一致的坯临,但是G服務的內(nèi)存表現(xiàn)很穩(wěn)定,并沒有F服務表現(xiàn)出來的內(nèi)存緩慢爬升的情況恋昼,而G服務表現(xiàn)內(nèi)存緩慢爬升則是隨著不斷提高流量灰度的一同上升的看靠。其中F服務的一個容器內(nèi)存情況大致是8臺內(nèi)存16G的云服務器,G服務的容器內(nèi)存情況是4臺8G的云服務器液肌。還有一個值得注意的一個點則是G服務的調(diào)用鏈路由于處于流量切換的過程與在F服務中不同衷笋,其區(qū)別如:
[圖片上傳失敗...(image-92d011-1727321763931)]
其中橙色的線表示G服務從F服務拆分出來后多一次交互,也就是說矩屁,在流量切換灰度期間辟宗,G服務的流量入口是從F服務通過rpc接口方式接受的。
此時的阿強大概了解了G服務應用的基本情況吝秕,接下來要做的事情則是去分析內(nèi)存緩慢爬升的問題泊脐,只見他拿出了一法器,此法器名叫“乾坤內(nèi)存鏡”烁峭,此法器的作用就是能夠清晰地分析應用內(nèi)存快照文文件容客,在使用法器有一個細節(jié)問題需要注意的是,如果通過此法器直接去打開內(nèi)存快照文件约郁,此法器會默認進行fullgc缩挑,fullgc后的快照文件如下圖:
[圖片上傳失敗...(image-a7acf0-1727321763931)]
fullgc后的快照文件內(nèi)存大小明顯和實際占用不同,如果想讓法器打開快照文件不盡興fullgc鬓梅,則只需要換一種打開方式供置,打開方式如下:
[圖片上傳失敗...(image-49b3ea-1727321763931)]
[圖片上傳失敗...(image-2b658f-1727321763931)][圖片上傳失敗...(image-8c092-1727321763931)]
[圖片上傳失敗...(image-a51259-1727321763931)]
此時你應該能看到如下圖的內(nèi)容說明此次打開方式?jīng)]有盡興fullgc,我們只需要稍等片刻即可
[圖片上傳失敗...(image-3a5d83-1727321763931)]
通過這種方式打開的快照文件則是如下所示:
[圖片上傳失敗...(image-d7254c-1727321763931)]
阿強看著解析出來的快照內(nèi)容绽快,此時展現(xiàn)出來內(nèi)容是通過內(nèi)存的實例的數(shù)量來進行的排序芥丧,其中紧阔,char[]占用了1412m大小的內(nèi)存,粗略看下來沒有什么大對象续担。如果是幾年前的阿強擅耽,他會傻不拉幾地去查看char[]實例的引用,但是此時的阿強已經(jīng)不是兩年前的阿強物遇,經(jīng)過時長兩年半的練習乖仇,他踩過數(shù)不清的坑,經(jīng)驗告訴他询兴,此時你應該看看第三個實例,阿強此時查看第三個實例的Merged outgoing references这敬,他看到此實例的引用
[圖片上傳失敗...(image-e245da-1727321763931)]
然后再進一步跟進String的引用,除了spring的常規(guī)引用蕉朵,發(fā)現(xiàn)logback和jackson有引用大量的字符實例崔涂。
[圖片上傳失敗...(image-a6212b-1727321763931)]
[圖片上傳失敗...(image-6d5dff-1727321763931)]
阿強此時通過idea打開了G服務的代碼,開始查看起來jackson和logback的代碼使用點始衅,2個時辰后冷蚂,阿強發(fā)現(xiàn)了一些奇怪的日志打印,如下:
log.info("業(yè)務接口處理請求參數(shù)明文汛闸,{}, http request method:{}",
JSON.toJSONString(request.getParams()), methodMapping.getMethod());
上面這種日志打印會將整個請求的入?yún)⒍即蛴〕鰜眚瑁乙淮握埱箢愃七@種打印全部請求入?yún)⒌娜杖罩敬蟾庞?~7次,而由于G服務的承載的業(yè)務請求報文都是比較大的诸老,也就是說隆夯,每一次請求過來,這種大日子的打印會打印好幾次别伏,而這些大而全的日志大部分內(nèi)容是沒用的蹄衷,并且每次打印生成的字符串每次都是不同的,也就是每次請求在堆內(nèi)存中生成6~7個大字符對象厘肮,這種大字符對象會在堆中頻繁創(chuàng)建愧口,會造成youngc很頻繁。而youngc過于頻繁會造成很多大字符對象進入老年代类茂,導致整個堆內(nèi)存不斷上升耍属。為了驗證自己的猜想,阿強嘗試著刪除G服務中這些大日志的打印巩检,最終發(fā)現(xiàn)內(nèi)存上升的情況有一定的改善(此時的內(nèi)存已經(jīng)不會出現(xiàn)緩慢爬坡的情況)厚骗,但是內(nèi)存表現(xiàn)相比較F服務還是沒有那么好的,因此阿強又只能進一步去分析內(nèi)存塊照文件兢哭,2刻鐘后领舰,阿強在線程ThreadLocal中發(fā)現(xiàn)很多大char[]數(shù)組的引用,而這些ThreadLocal都是由rpc線程所持有。
[圖片上傳失敗...(image-67126f-1727321763931)]
而rpc底層的序列化正是使用的jackson提揍,而com.fasterxml.jackson.core.util.BufferRecycler 是 Jackson 庫中的一個工具類,用于高效地管理和重用緩沖區(qū)煮仇。在多線程環(huán)境中劳跃,特別是使用 ThreadLocal 時,確實有可能導致內(nèi)存泄漏:
- ThreadLocal 的生命周期問題:ThreadLocal 變量會與線程的生命周期綁定浙垫,如果線程不被回收刨仑,ThreadLocal 變量及其引用的對象也不會被回收,可能導致內(nèi)存泄漏夹姥。
- 緩沖區(qū)的大小和數(shù)量:如果緩沖區(qū)的大小或數(shù)量非常大杉武,且這些緩沖區(qū)長期占用內(nèi)存而不被釋放,可能導致內(nèi)存使用過多辙售,從而引發(fā)內(nèi)存泄漏轻抱。
- 線程池使用不當:在使用線程池時,如果沒有正確管理線程池的生命周期和資源旦部,可能會導致線程無法被回收祈搜,進而導致 ThreadLocal 引用的對象無法被回收。
到這里真相大白士八,而阿強面對涉及基礎設施的改造容燕,他有點煩躁。凡是涉及基礎設施的改動婚度,任務的難度和解決時間就會成倍增加蘸秘,因為基礎設施的改造流程會拉的比較長。但這個任務是一個緊急的任務蝗茁,為了快速地將問題處理醋虏,那怎么能夠不去改造基礎設施并解決這個問題呢,阿強腦子在飛速的運轉哮翘,不多時灰粮,阿強心中閃過一絲光亮,他緊皺的眉間也開始舒坦忍坷。剛剛的那一絲光亮就是快速解決任務的關鍵粘舟,那就是:“類加載器的雙親委派機制!佩研!”