經驗之談:內存泄露的原因以及分析
內存泄露是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難以防范。
內存使用過高,一定是內存泄露嗎?
內存使用過高,並不一定是內存泄露導致的結果,具體要看內存堆的分析。
一般內存泄露最直觀的體現就是:
- 內存使用高
- GC回收不了內存,即GC前后堆大小幾乎無變化
- JVM瘋狂GC,CPU打滿
- Java進程觸發Linux操作系統的OOM-killer,Java進程被殺死
- 或者CPU被GC任務打滿,服務器實際宕機。
但是這不一定是泄露導致的,也有可能是內存的錯誤使用導致的,不過大同小異,主要還是需要排查異常內存的使用。
Ps:之所以作者這么說,是因為作者曾經在線上遇到了架構組修改日志框架,錯誤的將日志內容作為了key存入了map,本應的key-value應該為traceId-日志內容,結果架構組卻將key-value搞反了,導致大量的巨大key打滿了內存,堆dump文件里全是幾十k幾十k的字符串。
如何避免內存泄露
根據上面說的內存泄露多數發生的情況,避免內存泄露的策略也就十分簡單了。
- 盡量使用局部變量
- 減少使用static集合
- 如果必要的使用static集合,盡量使用弱引用等低級引用。比如參照ThreadLocal中的設計:TheadLocal原理
內存泄露問題如何排查
內存泄露或內存持續使用較高時,通常通過堆的情況來排查。
首先可以通過jmap -histo:live pid|less 命令,查看堆內對象使用情況。此時如果內存泄露,一般都是會某個基本類型對象過多,然后可以與正常的服務作對比,看哪個對象的數量異常的多,此時如果可以判斷出來,也沒必要dump了。
如果通過jmap無法斷定,則可以使用jmap -dump:live,format=b,file=
將dump文件通過java原生的軟件或者eclipse的mat工具,就可以看到哪些對象占用過多,此時你應該關注的是非基本類型對象的其他對象,因為一般來說都是基本類型的數量和大小最多。
一般來說,你會看到以下現象:
- 某個map的Node十分多,有幾十萬個。
- 某個框架的某個對象十分多。
- char數據,也就是C[],占用十分多,因為有很多大字符串。