天天看點

關于Java記憶體洩漏

   Java中由于有垃圾收集器(GC)自動回收資源,是以一般情況下不需要擔心記憶體洩漏的問題。這也是Java相對C/C++而言開發簡單高效的重要原因之一。想必調試過C/C++記憶體洩漏障害的人都深有體會。但是如果認為Java中不存在記憶體洩漏也是不對的。因為GC不是萬能的,也有它失靈的時候。那它什麼情況下會失靈呢?先簡單了解一下GC的工作方式吧。

   垃圾收集器每過一段時間就會将不再使用的資源回收。GC判斷一個對象是否是不再使用的方法非常重要。它以每個活的線程對象為根檢查有沒有能夠到達目标對象的引用鍊為判斷标準。如果不可達則說明該對象對程式(或者線程)的執行是不可見的,因為沒有任何手段可以引用該對象,自然這個對象肯定是不再被使用了。于是将其回收。

   簡單思考一下,上面的判斷邏輯隻是“什麼是不使用對象”的充分條件,而不是必要條件。至于必要條件是什麼,答案隻有開發者自己知道,因為隻有開發者自己才能準确知道哪些對象已經确實不再需要了,GC又怎麼能揣測到開發者的意圖呢?

   了解了這一點我們就知道了Java記憶體洩漏的第一種模式:GC不能正确識别出不再使用的對象。既:存在一條以活的線程對象(通常是主線程)為起點的引用鍊,指向了已經不再使用的對象。并且這些對象在程式運作中未能及時釋放後者數目不斷增長導緻記憶體消耗過大(如果記憶體消耗能始終保持在可接受的範圍之内,就談不上什麼記憶體洩漏了)。這樣的記憶體洩漏通常發生在一個集合對象上,尤其是作為高速緩存使用的集合對象。

   本來我以為剛才提到的模式是Java記憶體洩漏唯一的一種模式,但是一次實際經曆使我看到了第二種Java記憶體洩漏的模式:GC不能正常釋放它想釋放的對象。這是由于使用者重載了某個Java對象的finalize()方法,GC在釋放這個對象時就要調用這個被修改了的finalize()方法,而這個finalize()方法又由于某個原因離奇地無法結束(比如等待某個同步對象或者發生了死循環*),也就導緻這個對象沒法被正常釋放,于是記憶體洩漏自然就發生了。當然這種情況比起前面提到的第一種模式要少見的多。

* 我遇到的情況是finalize()方法在等待一個同步對象,但是該同步對象又被别的線程長時間霸占着。這個線程雖然也有釋放同步對象,但這個線程循環執行并且垃圾收集的線程優先級比較低,是以釋放了的同步對象很快又被它搶了去。并且這個線程在執行過程中又會建立新的修改了finalize()方法的Java對象,導緻該Java對象雖然也能被釋放,但釋放的速度跟不上建立的資料,時間久了,記憶體消耗就會越來越大,記憶體洩漏也就發生了。