Java內(nèi)存泄露的理解與解決
Java內(nèi)存管理機(jī)制
在C++ 語言中,如果需要?jiǎng)討B(tài)分配一塊內(nèi)存,程序員需要負(fù)責(zé)這塊內(nèi)存的整個(gè)生命周期。從申請分配、到使用、再到***的釋放。這樣的過程非常靈活,但是卻十分繁瑣,程序員很容易由于疏忽而忘記釋放內(nèi)存,從而導(dǎo)致內(nèi)存的泄露。 Java 語言對內(nèi)存管理做了自己的優(yōu)化,這就是垃圾回收機(jī)制。 Java 的幾乎所有內(nèi)存對象都是在堆內(nèi)存上分配(基本數(shù)據(jù)類型除外),然后由 GC ( garbage collection)負(fù)責(zé)自動(dòng)回收不再使用的內(nèi)存。
上面是Java 內(nèi)存管理機(jī)制的基本情況。但是如果僅僅理解到這里,我們在實(shí)際的項(xiàng)目開發(fā)中仍然會(huì)遇到內(nèi)存泄漏的問題。也許有人表示懷疑,既然 Java 的垃圾回收機(jī)制能夠自動(dòng)的回收內(nèi)存,怎么還會(huì)出現(xiàn)內(nèi)存泄漏的情況呢?這個(gè)問題,我們需要知道 GC 在什么時(shí)候回收內(nèi)存對象,什么樣的內(nèi)存對象會(huì)被 GC 認(rèn)為是“不再使用”的。
Java中對內(nèi)存對象的訪問,使用的是引用的方式。在 Java 代碼中我們維護(hù)一個(gè)內(nèi)存對象的引用變量,通過這個(gè)引用變量的值,我們可以訪問到對應(yīng)的內(nèi)存地址中的內(nèi)存對象空間。在 Java 程序中,這個(gè)引用變量本身既可以存放堆內(nèi)存中,又可以放在代碼棧的內(nèi)存中(與基本數(shù)據(jù)類型相同)。 GC 線程會(huì)從代碼棧中的引用變量開始跟蹤,從而判定哪些內(nèi)存是正在使用的。如果 GC 線程通過這種方式,無法跟蹤到某一塊堆內(nèi)存,那么 GC 就認(rèn)為這塊內(nèi)存將不再使用了(因?yàn)榇a中已經(jīng)無法訪問這塊內(nèi)存了)。
通過這種有向圖的內(nèi)存管理方式,當(dāng)一個(gè)內(nèi)存對象失去了所有的引用之后,GC 就可以將其回收。反過來說,如果這個(gè)對象還存在引用,那么它將不會(huì)被 GC 回收,哪怕是 Java 虛擬機(jī)拋出 OutOfMemoryError 。
- Vector v = new Vector( 10 );
- for ( int i = 1 ;i < 100 ; i ++ ){
- Object o = new Object();
- v.add(o);
- o = null ;
- }
在這個(gè)例子中,代碼棧中存在Vector 對象的引用 v 和 Object 對象的引用 o 。在 For 循環(huán)中,我們不斷的生成新的對象,然后將其添加到 Vector 對象中,之后將 o 引用置空。問題是當(dāng) o 引用被置空后,如果發(fā)生 GC ,我們創(chuàng)建的 Object 對象是否能夠被 GC 回收呢?答案是否定的。因?yàn)椋?GC 在跟蹤代碼棧中的引用時(shí),會(huì)發(fā)現(xiàn) v 引用,而繼續(xù)往下跟蹤,就會(huì)發(fā)現(xiàn) v 引用指向的內(nèi)存空間中又存在指向 Object 對象的引用。也就是說盡管 o 引用已經(jīng)被置空,但是 Object 對象仍然存在其他的引用,是可以被訪問到的,所以 GC 無法將其釋放掉。如果在此循環(huán)之后, Object 對象對程序已經(jīng)沒有任何作用,那么我們就認(rèn)為此 Java 程序發(fā)生了內(nèi)存泄漏。
盡管對于C/C++ 中的內(nèi)存泄露情況來說, Java 內(nèi)存泄露導(dǎo)致的破壞性小,除了少數(shù)情況會(huì)出現(xiàn)程序崩潰的情況外,大多數(shù)情況下程序仍然能正常運(yùn)行。但是,在移動(dòng)設(shè)備對于內(nèi)存和 CPU 都有較嚴(yán)格的限制的情況下, Java 的內(nèi)存溢出會(huì)導(dǎo)致程序效率低下、占用大量不需要的內(nèi)存等問題。這將導(dǎo)致整個(gè)機(jī)器性能變差,嚴(yán)重的也會(huì)引起拋出 OutOfMemoryError ,導(dǎo)致程序崩潰。
一般情況下內(nèi)存泄漏的避免
在不涉及復(fù)雜數(shù)據(jù)結(jié)構(gòu)的一般情況下,Java 的內(nèi)存泄露表現(xiàn)為一個(gè)內(nèi)存對象的生命周期超出了程序需要它的時(shí)間長度。我們有時(shí)也將其稱為“對象游離”。
例如:
- public class FileSearch{
- private byte [] content;
- private File mFile;
- public FileSearch(File file){
- mFile = file;
- }
- public boolean hasString(String str){
- int size = getFileSize(mFile);
- content = new byte [size];
- loadFile(mFile, content);
- String s = new String(content);
- return s.contains(str);
- }
- }
在這段代碼中,F(xiàn)ileSearch 類中有一個(gè)函數(shù) hasString ,用來判斷文檔中是否含有指定的字符串。流程是先將mFile 加載到內(nèi)存中,然后進(jìn)行判斷。但是,這里的問題是,將 content 聲明為了實(shí)例變量,而不是本地變量。于是,在此函數(shù)返回之后,內(nèi)存中仍然存在整個(gè)文件的數(shù)據(jù)。而很明顯,這些數(shù)據(jù)我們后續(xù)是不再需要的,這就造成了內(nèi)存的無故浪費(fèi)。
要避免這種情況下的內(nèi)存泄露,要求我們以C/C++ 的內(nèi)存管理思維來管理自己分配的內(nèi)存。***,是在聲明對象引用之前,明確內(nèi)存對象的有效作用域。在一個(gè)函數(shù)內(nèi)有效的內(nèi)存對象,應(yīng)該聲明為 local 變量,與類實(shí)例生命周期相同的要聲明為實(shí)例變量……以此類推。第二,在內(nèi)存對象不再需要時(shí),記得手動(dòng)將其引用置空。
復(fù)雜數(shù)據(jù)結(jié)構(gòu)中的內(nèi)存泄露問題
在實(shí)際的項(xiàng)目中,我們經(jīng)常用到一些較為復(fù)雜的數(shù)據(jù)結(jié)構(gòu)用于緩存程序運(yùn)行過程中需要的數(shù)據(jù)信息。有時(shí),由于數(shù)據(jù)結(jié)構(gòu)過于復(fù)雜,或者我們存在一些特殊的需求(例如,在內(nèi)存允許的情況下,盡可能多的緩存信息來提高程序的運(yùn)行速度等情況),我們很難對數(shù)據(jù)結(jié)構(gòu)中數(shù)據(jù)的生命周期作出明確的界定。這個(gè)時(shí)候,我們可以使用Java 中一種特殊的機(jī)制來達(dá)到防止內(nèi)存泄露的目的。
之前我們介紹過,Java 的 GC 機(jī)制是建立在跟蹤內(nèi)存的引用機(jī)制上的。而在此之前,我們所使用的引用都只是定義一個(gè)“ Object o; ”這樣形式的。事實(shí)上,這只是 Java 引用機(jī)制中的一種默認(rèn)情況,除此之外,還有其他的一些引用方式。通過使用這些特殊的引用機(jī)制,配合 GC 機(jī)制,就可以達(dá)到一些我們需要的效果。
#p#
Java中的幾種引用方式
Java中有幾種不同的引用方式,它們分別是:強(qiáng)引用、軟引用、弱引用和虛引用。下面,我們首先詳細(xì)地了解下這幾種引用方式的意義。
強(qiáng)引用
在此之前我們介紹的內(nèi)容中所使用的引用 都是強(qiáng)引用,這是使用最普遍的引用。如果一個(gè)對象具有強(qiáng)引用,那就類似于必不可少的生活用品,垃圾回收器絕不會(huì)回收它。當(dāng)內(nèi)存空 間不足,Java 虛擬機(jī)寧愿拋出 OutOfMemoryError 錯(cuò)誤,使程序異常終止,也不會(huì)靠隨意回收具有強(qiáng)引用的對象來解決內(nèi)存不足問題。
軟引用(SoftReference )
SoftReference 類的一個(gè)典型用途就是用于內(nèi)存敏感的高速緩存。 SoftReference 的原理是:在保持對對象的引用時(shí)保證在 JVM 報(bào)告內(nèi)存不足情況之前將清除所有的軟引用。關(guān)鍵之處在于,垃圾收集器在運(yùn)行時(shí)可能會(huì)(也可能不會(huì))釋放軟可及對象。對象是否被釋放取決于垃圾收集器的算法 以及垃圾收集器運(yùn)行時(shí)可用的內(nèi)存數(shù)量。
弱引用(WeakReference )
WeakReference 類的一個(gè)典型用途就是規(guī)范化映射( canonicalized mapping )。另外,對于那些生存期相對較長而且重新創(chuàng)建的開銷也不高的對象來說,弱引用也比較有用。關(guān)鍵之處在于,垃圾收集器運(yùn)行時(shí)如果碰到了弱可及對象,將釋放 WeakReference 引用的對象。然而,請注意,垃圾收集器可能要運(yùn)行多次才能找到并釋放弱可及對象。
虛引用(PhantomReference )
PhantomReference 類只能用于跟蹤對被引用對象即將進(jìn)行的收集。同樣,它還能用于執(zhí)行 pre-mortem 清除操作。 PhantomReference 必須與 ReferenceQueue 類一起使用。需要 ReferenceQueue 是因?yàn)樗軌虺洚?dāng)通知機(jī)制。當(dāng)垃圾收集器確定了某個(gè)對象是虛可及對象時(shí), PhantomReference 對象就被放在它的 ReferenceQueue 上。將 PhantomReference 對象放在 ReferenceQueue 上也就是一個(gè)通知,表明 PhantomReference 對象引用的對象已經(jīng)結(jié)束,可供收集了。這使您能夠剛好在對象占用的內(nèi)存被回收之前采取行動(dòng)。 Reference與 ReferenceQueue 的配合使用。
GC、 Reference 與 ReferenceQueue 的交互
3、軟引用和弱引用在添加到 ReferenceQueue 的時(shí)候,其指向真實(shí)內(nèi)存的引用已經(jīng)被置為空了,相關(guān)的內(nèi)存也已經(jīng)被釋放掉了。而虛引用在添加到 ReferenceQueue 的時(shí)候,內(nèi)存還沒有釋放,仍然可以對其進(jìn)行訪問。
- String str = new String( " hello " ); // ①
- ReferenceQueue < String > rq = new ReferenceQueue < String > (); // ②
- WeakReference < String > wf = new WeakReference < String > (str, rq); // ③
- str = null ; // ④取消"hello"對象的強(qiáng)引用
- String str1 = wf.get(); // ⑤假如"hello"對象沒有被回收,str1引用"hello"對象
- // 假如"hello"對象沒有被回收,rq.poll()返回null
- Reference <? extends String > ref = rq.poll(); // ⑥
在以上代碼中,注意⑤⑥兩處地方。假如“hello ”對象沒有被回收 wf.get() 將返回“ hello ”字符串對象, rq.poll() 返回 null ;而加入“ hello ”對象已經(jīng)被回收了,那么 wf.get() 返回 null , rq.poll() 返回 Reference 對象,但是此 Reference 對象中已經(jīng)沒有 str 對象的引用了 ( PhantomReference 則與WeakReference 、 SoftReference 不同 )。
引用機(jī)制與復(fù)雜數(shù)據(jù)結(jié)構(gòu)的聯(lián)合應(yīng)用
了解了GC 機(jī)制、引用機(jī)制,并配合上 ReferenceQueue ,我們就可以實(shí)現(xiàn)一些防止內(nèi)存溢出的復(fù)雜數(shù)據(jù)類型。
例如,SoftReference 具有構(gòu)建 Cache 系統(tǒng)的特質(zhì),因此我們可以結(jié)合哈希表實(shí)現(xiàn)一個(gè)簡單的緩存系統(tǒng)。這樣既能保證能夠盡可能多的緩存信息,又可以保證 Java 虛擬機(jī)不會(huì)因?yàn)閮?nèi)存泄露而拋出 OutOfMemoryError 。這種緩存機(jī)制特別適合于內(nèi)存對象生命周期長,且生成內(nèi)存對象的耗時(shí)比較長的情況,例如緩存列表封面圖片等。對于一些生命周期較長,但是生成內(nèi)存對象開銷不大的情況,使用WeakReference 能夠達(dá)到更好的內(nèi)存管理的效果。
#p#
附SoftHashmap 的源碼一份,相信看過之后,大家會(huì)對 Reference 機(jī)制的應(yīng)用有更深入的理解。
- package com. *** .widget;
- // : SoftHashMap.java
- import java.util. * ;
- import java.lang.ref. * ;
- import android.util.Log;
- public class SoftHashMap extends AbstractMap {
- /** The internal HashMap that will hold the SoftReference. */
- private final Map hash = new HashMap();
- /** The number of "hard" references to hold internally. */
- private final int HARD_SIZE;
- /** The FIFO list of hard references, order of last access. */
- private final LinkedList hardCache = new LinkedList();
- /** Reference queue for cleared SoftReference objects. */
- private ReferenceQueue queue = new ReferenceQueue();
- // Strong Reference number
- public SoftHashMap() { this ( 100 ); }
- public SoftHashMap( int hardSize) { HARD_SIZE = hardSize; }
- public Object get(Object key) {
- Object result = null ;
- // We get the SoftReference represented by that key
- SoftReference soft_ref = (SoftReference)hash.get(key);
- if (soft_ref != null ) {
- // From the SoftReference we get the value, which can be
- // null if it was not in the map, or it was removed in
- // the processQueue() method defined below
- result = soft_ref.get();
- if (result == null ) {
- // If the value has been garbage collected, remove the
- // entry from the HashMap.
- hash.remove(key);
- } else {
- // We now add this object to the beginning of the hard
- // reference queue. One reference can occur more than
- // once, because lookups of the FIFO queue are slow, so
- // we don't want to search through it each time to remove
- // duplicates.
- // keep recent use object in memory
- hardCache.addFirst(result);
- if (hardCache.size() > HARD_SIZE) {
- // Remove the last entry if list longer than HARD_SIZE
- hardCache.removeLast();
- }
- }
- }
- return result;
- }
- /** We define our own subclass of SoftReference which contains
- not only the value but also the key to make it easier to find
- the entry in the HashMap after it's been garbage collected. */
- private static class SoftValue extends SoftReference {
- private final Object key; // always make data member final
- /** Did you know that an outer class can access private data
- members and methods of an inner class? I didn't know that!
- I thought it was only the inner class who could access the
- outer class's private information. An outer class can also
- access private members of an inner class inside its inner
- class. */
- private SoftValue(Object k, Object key, ReferenceQueue q) {
- super (k, q);
- this .key = key;
- }
- }
- /** Here we go through the ReferenceQueue and remove garbage
- collected SoftValue objects from the HashMap by looking them
- up using the SoftValue.key data member. */
- public void processQueue() {
- SoftValue sv;
- while ((sv = (SoftValue)queue.poll()) != null ) {
- if (sv.get() == null ) {
- Log.e( " processQueue " , " null " );
- } else {
- Log.e( " processQueue " , " Not null " );
- }
- hash.remove(sv.key); // we can access private data!
- Log.e( " SoftHashMap " , " release " + sv.key);
- }
- }
- /** Here we put the key, value pair into the HashMap using
- a SoftValue object. */
- public Object put(Object key, Object value) {
- processQueue(); // throw out garbage collected values first
- Log.e( " SoftHashMap " , " put into " + key);
- return hash.put(key, new SoftValue(value, key, queue));
- }
- public Object remove(Object key) {
- processQueue(); // throw out garbage collected values first
- return hash.remove(key);
- }
- public void clear() {
- hardCache.clear();
- processQueue(); // throw out garbage collected values
- hash.clear();
- }
- public int size() {
- processQueue(); // throw out garbage collected values first
- return hash.size();
- }
- public Set entrySet() {
- // no, no, you may NOT do that!!! GRRR
- throw new UnsupportedOperationException();
- }
- }
原文鏈接:http://henryyang.iteye.com/blog/1188328
編輯推薦:
- 什么是JavaScript異步編程
- 使用HTML 5和Javascript設(shè)計(jì)繪圖程序
- 多核平臺(tái)下的Java優(yōu)化
- Java開發(fā)框架Play框架快速入門
- 利用Java實(shí)現(xiàn)電子郵件的批量發(fā)送