Java的內存泄露


Java有垃圾回收,因此不會出現內存泄露。

大錯特錯。


這個說法存在好幾個問題。儘管Java的確有垃圾回收器來回收那些不用的內存塊,但你不要指望它能夠點鐵成金。GC減輕了開發人員肩上的負擔,而原本的那些工作非常容易出錯,不過並不是所有內存分配的問題它都能夠解決。更糟糕的是,Java的設計允許它可以欺騙GC,使得它能夠保留一些程序已經不再使用的內存。經歷了20年的C開發以及7年的Java開發後(中間有重疊),我敢說,在這方面Java絕對是遠比C/C++要好。儘管它仍有改進的空間。在這些改進成爲現實之前,作爲開發人員最好能瞭解下內存處理的基本原理以及一些常見的坑,以免栽到裏面去。但首先,

什麼是內存泄露?



內存泄露是指程序不停地分配內存,但不再使用的時候卻沒有釋放掉它,這會導致本來就有限的內存的佔用量出現飆升,並且這不受程序控制,最終導致程序的運行變慢。


在那些美好的C語言開發的時代,我們說的內存泄露是指程序遺失了某個內存段的引用而沒有釋放掉它。這種情況下,程序獲取不到這個內存區域的句柄或者指針,也無法調用free函數來釋放掉它,因此這個內存塊會一直處於分配的狀態,沒法被程序重用,這樣就造成了內存的浪費。當然了,程序退出的話,操作系統會回收掉這塊內存的。

這是個非常典型的內存泄露,不過我上面給出的定義更廣泛一些。還有一種情況是代碼仍舊擁有這塊內存的指針,儘管現在這塊內存已經不用了,但程序也不去釋放它。就比如說一個程序員創建了一個鏈表,把所有通過malloc分配的內存指針全存了進去,但他從來不去調用free函數釋放掉它們。結果也是一樣的。既然結果是一樣的,能不能獲取到釋放內存的指針也不那麼重要了,因爲你根本就不去釋放它。這只是影響到了解決問題的方式,不過不管是哪種情況,修復BUG總是得修改代碼的。

如果我們來看下Java和它的GC,你會發現經典的那個由於釋放了內存引用導致無法釋放內存的那種情況幾乎不可能發生。如果是那樣的話GC判斷出分配的內存的所有引用已經釋放掉了就自己去釋放內存了。事實上,這也是Java裏面標準的釋放內存的方法:你只需不再引用某個對象就可以了,GC會去回收它的。這裏沒有垃圾桶,也沒有分類垃圾箱(不需要你去扔垃圾)。別管它就行了,垃圾回收器會去回收它的。這也正是很多開發人員認爲Java不存在內存泄露的原因。從實際的角度來看這的確幾乎是正確的:和使用C/C++或者其它沒有垃圾回收器的語言相比,使用Java開發內存泄露的麻煩事的確少了不少。

我們終於要說到重點了:Java裏面是如何發生內存泄露的?

線程以及線程本地存儲就非常容易產生內存泄露。通過下面的五個步驟可以很容易地產生內存泄露:

1. 應用程序創建一個長時間運行的線程(或者使用線程池,那樣泄露會更快一些)。
2. 線程通過某個類加載器加載了一個類。
3. 這個類分配了一大塊內存(比如,new byte[1000000]),並且在它的靜態字段中存儲了一個強引用,然後在ThreadLocal中存儲它自身的一個引用。額外分配的這個內存(new byte[1000000])其實是多餘的(類實例的泄露就足夠了),不過這樣能使內存泄露的速度更快一些。
4. 線程清理了自定義的類以及加載它的類加載器的所有引用。
5. 重複以上步驟。

因爲你已經沒有這個類以及它的類加載器的引用了,也就不能再訪問它的ThreadLocal中的存儲了,因此你也就無法訪問分配的這塊內存(除非你開始用反射來獲取)。但是這個ThreadLocal的存儲還存在引用,因此GC無法回收這塊內存。這個線程本地的存儲不是弱引用(順便提一句,爲什麼不用弱引用?)

如果你從來沒有類似的經驗,你可能會想,這得多腦殘才能搞出這麼極端的一個場景。但事實上,上述這種泄露的模式是非常自然的(好吧,程序員們,你們可別故意這麼搞 ),當你在Tomcat上調試自己的程序的時候就會出現這樣的泄露。對Java來說這太正常了。重新部署應用但卻不重啓Tomcat實例,這通常會導致內存越來越少,這正是發生了上述的這種泄露,很少有Tomcat能夠避免這種情況。應用程序應當謹慎地使用ThreadLocal。

使用靜態變量存儲大塊數據時也應當同樣小心。最好避免使用靜態變量,除非你很相信這個運行你程序的容器不會發生泄露。這些容器的類加載的層次結構和Java的比起來要靈活多了。如果你把大量的數據存儲到一個Map或者Set裏,爲什麼不使用它們的弱引用的版本呢?如果KEY都沒了,還需要關聯的那個值幹嘛?

現在來說下HashMap和HashSet。如果你使用了沒有實現或者錯誤地實現了eqauls和hashCode方法的對象來作爲KEY的話,調用put()方法會把你的數據扔向深淵。你再也沒法恢復它了,更糟糕的是,每當你再放一個對象到這個集合中的時候,還會產生更多的副本。你把你的內存帶上了一條不歸路。

在Java中,還有許許多多的內存泄露的例子。儘管它們和C/C++相比,出現的頻率要少得多。通常來說,有GC總比沒有的要好。
發佈了3 篇原創文章 · 獲贊 3 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章