經驗之談:記憶體洩露的原因以及分析

IntoTw發表於2022-03-17

經驗之談:記憶體洩露的原因以及分析

記憶體洩露是Javaer聽到最多的關於記憶體的事了,這篇文章就來談談這件事。

記憶體洩露與資源洩露

什麼是洩露?洩露在計算機語境下,通常指的是某個資源無法被訪問,也無法被釋放。

記憶體洩露一般發生在某個物件的引用丟失,無法再訪問到該引用,但是該引用卻依舊引用著某個物件,導致這個物件無法回收,最終導致記憶體溢位OOM。

資源洩露一般發生在連線池,IO流等場景,如從連線池中每次都新建連線但不關閉,每次都開啟新的IO流但不關閉,等等情況。

記憶體洩露發生的情況

記憶體洩露多發生於static的集合中,比如當你定義了一個static HashMap,此時將某個key-value放入其中後,方法段結束。

這時,除非呼叫map的clear方法,否則顯然該value將無限持有物件的引用,無法釋放。

public static Map<String,Object> objectMap=new HashMap<>();
public static void main(String[] args) {
    Integer a=new Integer(1);
    objectMap.put("testKey",a);
}

這種寫法看似可笑,卻很難避免,尤其在大量框架程式碼中,反而更容易發生,因為大部分框架程式碼對業務程式碼的增強,都是通過AOP方式來做的,此時對業務程式碼來說,這類隱式的static Map難以防範。

記憶體使用過高,一定是記憶體洩露嗎?

記憶體使用過高,並不一定是記憶體洩露導致的結果,具體要看記憶體堆的分析。

一般記憶體洩露最直觀的體現就是:

  1. 記憶體使用高
  2. GC回收不了記憶體,即GC前後堆大小几乎無變化
  3. JVM瘋狂GC,CPU打滿
  4. Java程式觸發Linux作業系統的OOM-killer,Java程式被殺死
  5. 或者CPU被GC任務打滿,伺服器實際當機。

但是這不一定是洩露導致的,也有可能是記憶體的錯誤使用導致的,不過大同小異,主要還是需要排查異常記憶體的使用。

Ps:之所以作者這麼說,是因為作者曾經線上上遇到了架構組修改日誌框架,錯誤的將日誌內容作為了key存入了map,本應的key-value應該為traceId-日誌內容,結果架構組卻將key-value搞反了,導致大量的巨大key打滿了記憶體,堆dump檔案裡全是幾十k幾十k的字串。

如何避免記憶體洩露

根據上面說的記憶體洩露多數發生的情況,避免記憶體洩露的策略也就十分簡單了。

  1. 儘量使用區域性變數
  2. 減少使用static集合
  3. 如果必要的使用static集合,儘量使用弱引用等低階引用。比如參照ThreadLocal中的設計:TheadLocal原理

記憶體洩露問題如何排查

記憶體洩露或記憶體持續使用較高時,通常通過堆的情況來排查。

首先可以通過jmap -histo:live pid|less 命令,檢視堆內物件使用情況。此時如果記憶體洩露,一般都是會某個基本型別物件過多,然後可以與正常的服務作對比,看哪個物件的數量異常的多,此時如果可以判斷出來,也沒必要dump了。

如果通過jmap無法斷定,則可以使用jmap -dump:live,format=b,file= 命令,生成dump檔案。

將dump檔案通過java原生的軟體或者eclipse的mat工具,就可以看到哪些物件佔用過多,此時你應該關注的是非基本型別物件的其他物件,因為一般來說都是基本型別的數量和大小最多。

一般來說,你會看到以下現象:

  1. 某個map的Node十分多,有幾十萬個。
  2. 某個框架的某個物件十分多。
  3. char資料,也就是C[],佔用十分多,因為有很多大字串。

相關文章