JAVA 内存泄露详解(原因、例子及解决)
??Java的一個重要特性就是通過垃圾收集器(GC)自動管理內存的回收,而不需要程序員自己來釋放內存。理論上Java中所有不會再被利用的對象所占用的內存,都可以被GC回收,但是Java也存在內存泄露,但它的表現與C++不同。
JAVA 中的內存管理
? ? 要了解Java中的內存泄露,首先就得知道Java中的內存是如何管理的。
? ? 在Java程序中,我們通常使用new為對象分配內存,而這些內存空間都在堆(Heap)上。
? ? 下面看一個示例:
? ??Java使用有向圖的方式進行內存管理:
? ??
? ? 在有向圖中,我們叫作obj1是可達的,obj2就是不可達的,顯然不可達的可以被清理。
? ? 內存的釋放,也即清理那些不可達的對象,是由GC決定和執行的,所以GC會監控每一個對象的狀態,包括申請、引用、被引用和賦值等。釋放對象的根本原則就是對象不會再被使用:
- ? ? 給對象賦予了空值null,之后再沒有調用過。
- ? ? 另一個是給對象賦予了新值,這樣重新分配了內存空間。
? ? 通常,會認為在堆上分配對象的代價比較大,但是GC卻優化了這一操作:C++中,在堆上分配一塊內存,會查找一塊適用的內存加以分配,如果對象銷毀,這塊內存就可以重用;而Java中,就想一條長的帶子,每分配一個新的對象,Java的“堆指針”就向后移動到尚未分配的區域。所以,Java分配內存的效率,可與C++媲美。
? ? 但是這種工作方式有一個問題:如果頻繁的申請內存,資源將會耗盡。這時GC就介入了進來,它會回收空間,并使堆中的對象排列更緊湊。這樣,就始終會有足夠大的內存空間可以分配。
? ? gc清理時的引用計數方式:當引用連接至新對象時,引用計數+1;當某個引用離開作用域或被設置為null時,引用計數-1,GC發現這個計數為0時,就回收其占用的內存。這個開銷會在引用程序的整個生命周期發生,并且不能處理循環引用的情況。所以這種方式只是用來說明GC的工作方式,而不會被任何一種Java虛擬機應用。
? ? 多數GC采用一種自適應的清理方式(加上其他附加的用于提升速度的技術),主要依據是找出任何“活”的對象,然后采用“自適應的、分代的、停止-復制、標記-清理”式的垃圾回收器。具體不介紹太多,這不是本文重點。
?
JAVA 中的內存泄露
? ? Java中的內存泄露,廣義并通俗的說,就是:不再會被使用的對象的內存不能被回收,就是內存泄露。
? ? Java中的內存泄露與C++中的表現有所不同。
? ? 在C++中,所有被分配了內存的對象,不再使用后,都必須程序員手動的釋放他們。所以,每個類,都會含有一個析構函數,作用就是完成清理工作,如果我們忘記了某些對象的釋放,就會造成內存泄露。
? ? 但是在Java中,我們不用(也沒辦法)自己釋放內存,無用的對象由GC自動清理,這也極大的簡化了我們的編程工作。但,實際有時候一些不再會被使用的對象,在GC看來不能被釋放,就會造成內存泄露。
? ? 我們知道,對象都是有生命周期的,有的長,有的短,如果長生命周期的對象持有短生命周期的引用,就很可能會出現內存泄露。我們舉一個簡單的例子:
? ? 這里的object實例,其實我們期望它只作用于method1()方法中,且其他地方不會再用到它,但是,當method1()方法執行完成后,object對象所分配的內存不會馬上被認為是可以被釋放的對象,只有在Simple類創建的對象被釋放后才會被釋放,嚴格的說,這就是一種內存泄露。解決方法就是將object作為method1()方法中的局部變量。當然,如果一定要這么寫,可以改為這樣:
? ? 這樣,之前“new Object()”分配的內存,就可以被GC回收。
? ? 到這里,Java的內存泄露應該都比較清楚了。下面再進一步說明:
- ? ? 在堆中的分配的內存,在沒有將其釋放掉的時候,就將所有能訪問這塊內存的方式都刪掉(如指針重新賦值),這是針對c++等語言的,Java中的GC會幫我們處理這種情況,所以我們無需關心。
- ? ? 在內存對象明明已經不需要的時候,還仍然保留著這塊內存和它的訪問方式(引用),這是所有語言都有可能會出現的內存泄漏方式。編程時如果不小心,我們很容易發生這種情況,如果不太嚴重,可能就只是短暫的內存泄露。
?
一些容易發生內存泄露的例子和解決方法
? ? 像上面例子中的情況很容易發生,也是我們最容易忽略并引發內存泄露的情況,解決的原則就是盡量減小對象的作用域(比如Android?studio中,上面的代碼就會發出警告,并給出的建議是將類的成員變量改寫為方法內的局部變量)以及手動設置null值。
? ? 至于作用域,需要在我們編寫代碼時多注意;null值的手動設置,我們可以看一下Java容器LinkedList源碼(可參考:Java之LinkedList源碼解讀(JDK 1.8))的刪除指定節點的內部方法:
? ? 除了修改節點間的關聯關系,我們還要做的就是賦值為null的操作,不管GC何時會開始清理,我們都應及時的將無用的對象標記為可被清理的對象。
? ? 我們知道Java容器ArrayList是數組實現的(可參考:Java之ArrayList源碼解讀(JDK 1.8)),如果我們要為其寫一個pop()(彈出)方法,可能會是這樣:
? ? 寫法很簡潔,但這里卻會造成內存溢出:elementData[size-1]依然持有E類型對象的引用,并且暫時不能被GC回收。我們可以如下修改:
? ? 我們寫代碼并不能一味的追求簡潔,首要是保證其正確性。
? ? 容器使用時的內存泄露
? ? 在很多文章中可能看到一個如下內存泄露例子:
? ? 可能很多人一開始并不理解,下面我們將上面的代碼完整一下就好理解了:
? ? 這里內存泄露指的是在對vector操作完成之后,執行下面與vector無關的代碼時,如果發生了GC操作,這一系列的object是沒法被回收的,而此處的內存泄露可能是短暫的,因為在整個method()方法執行完成后,那些對象還是可以被回收。這里要解決很簡單,手動賦值為null即可:
? ? 上面Vector已經過時了,不過只是使用老的例子來做內存泄露的介紹。我們使用容器時很容易發生內存泄露,就如上面的例子,不過上例中,容器時方法內的局部變量,造成的內存泄漏影響可能不算很大(但我們也應該避免),但是,如果這個容器作為一個類的成員變量,甚至是一個靜態(static)的成員變量時,就要更加注意內存泄露了。
? ? 下面也是一種使用容器時可能會發生的錯誤:
? ? 運行上面的代碼將很快報錯:
? ? 如果足夠了解Java的容器,上面的錯誤是不可能發生的。這里也推薦一篇本人介紹Java容器的文章:...
? ? 容器Set只存放唯一的元素,是通過對象的equals()方法來比較的,但是Java中所有類都直接或間接繼承至Object類,Object類的equals()方法比較的是對象的地址,上例中,就會一直添加元素直到內存溢出。
? ? 所以,上例嚴格的說是容器的錯誤使用導致的內存溢出。
? ? 就Set而言,remove()方法也是通過equals()方法來刪除匹配的元素的,如果一個對象確實提供了正確的equals()方法,但是切記不要在修改這個對象后使用remove(Object o),這也可能會發生內存泄露。
? ? 各種提供了close()方法的對象
? ??比如數據庫連接(dataSourse.getConnection()),網絡連接(socket)和io連接,以及使用其他框架的時候,除非其顯式的調用了其close()方法(或類似方法)將其連接關閉,否則是不會自動被GC回收的。其實原因依然是長生命周期對象持有短生命周期對象的引用。
? ? 可能很多人使用過hibernate,我們操作數據庫時,通過SessionFactory獲取一個session:
? ? 完成后我們必須調用close()方法關閉:
? ? SessionFactory就是一個長生命周期的對象,而session相對是個短生命周期的對象,但是框架這么設計是合理的:它并不清楚我們要使用session到多久,于是只能提供一個方法讓我們自己決定何時不再使用。
? ? 因為在close()方法調用之前,可能會拋出異常而導致方法不能被調用,我們通常使用try語言,然后再finally語句中執行close()等清理工作:
? ? 單例模式導致的內存泄露
? ? 單例模式,很多時候我們可以把它的生命周期與整個程序的生命周期看做差不多的,所以是一個長生命周期的對象。如果這個對象持有其他對象的引用,也很容易發生內存泄露。
? ? 內部類和外部模塊的引用
? ? 其實原理依然是一樣的,只是出現的方式不一樣而已。
?
與清理相關的方法
? ? 本節主要談論gc()和finalize()方法。
? ? gc()
? ??對于程序員來說,GC基本是透明的,不可見的。運行GC的函數是System.gc(),調用后啟動垃圾回收器開始清理。
? ? 但是根據Java語言規范定義, 該函數不保證JVM的垃圾收集器一定會執行。因為,不同的JVM實現者可能使用不同的算法管理GC。通常,GC的線程的優先級別較低。
? ? JVM調用GC的策略也有很多種,有的是內存使用到達一定程度時,GC才開始工作,也有定時執行的,有的是平緩執行GC,有的是中斷式執行GC。但通常來說,我們不需要關心這些。除非在一些特定的場合,GC的執行影響應用程序的性能,例如對于基于Web的實時系統,如網絡游戲等,用戶不希望GC突然中斷應用程序執行而進行垃圾回收,那么我們需要調整GC的參數,讓GC能夠通過平緩的方式釋放內存,例如將垃圾回收分解為一系列的小步驟執行,Sun提供的HotSpot JVM就支持這一特性。
? ? finalize()
? ? finalize()是Object類中的方法。
? ? 了解C++的都知道有個析構函數,但是注意,finalize()絕不等于C++中的析構函數。
? ? Java編程思想中是這么解釋的:一旦GC準備好釋放對象所占用的的存儲空間,將先調用其finalize()方法,并在下一次GC回收動作發生時,才會真正回收對象占用的內存,所以一些清理工作,我們可以放到finalize()中。
? ? 該方法的一個重要的用途是:當在java中調用非java代碼(如c和c++)時,在這些非java代碼中可能會用到相應的申請內存的操作(如c的malloc()函數),而在這些非java代碼中并沒有有效的釋放這些內存,就可以使用finalize()方法,并在里面調用本地方法的free()等函數。
? ? 所以finalize()并不適合用作普通的清理工作。
? ? 不過有時候,該方法也有一定的用處:
? ? 如果存在一系列對象,對象中有一個狀態為false,如果我們已經處理過這個對象,狀態會變為true,為了避免有被遺漏而沒有處理的對象,就可以使用finalize()方法:
? ? 但是從很多方面了解,該方法都是被推薦不要使用的,并被認為是多余的。
總結
以上是生活随笔為你收集整理的JAVA 内存泄露详解(原因、例子及解决)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: inline内联的用法与作用
- 下一篇: 解决VS2010复制代码中文乱码的问题