Java中的內存泄露問題和解決辦法

(Memory Leak,內存泄漏)

為什麼會產生內存泄漏?

當一個對象已經不需要再使用本該被回收時,另外一個正在使用的對象持有它的引用從而導致它不能被回收,這導致本該被回收的對象不能被回收而停留在堆內存中,這就產生瞭內存泄漏。

內存泄漏對程序的影響?

內存泄漏是造成應用程序OOM的主要原因之一。我們知道Android系統為每個應用程序分配的內存是有限的,而當一個應用中產生的內存泄漏比較多時,這就難免會導致應用所需要的內存超過系統分配的內存限額,這就造成瞭內存溢出從而導致應用Crash。

如何檢查和分析內存泄漏?

因為內存泄漏是在堆內存中,所以對我們來說並不是可見的。通常我們可以借助MAT、LeakCanary等工具來檢測應用程序是否存在內存泄漏。
1、MAT是一款強大的內存分析工具,功能繁多而復雜。
2、LeakCanary則是由Square開源的一款輕量級的第三方內存泄漏檢測工具,當檢測到程序中產生內存泄漏時,它將以最直觀的方式告訴我們哪裡產生瞭內存泄漏和導致誰泄漏瞭而不能被回收。

常見的內存泄漏及解決方法

1、單例造成的內存泄漏

由於單例的靜態特性使得其生命周期和應用的生命周期一樣長,如果一個對象已經不再需要使用瞭,而單例對象還持有該對象的引用,就會使得該對象不能被正常回收,從而導致瞭內存泄漏。
示例:防止單例導致內存泄漏的實例

// 使用瞭單例模式
public class AppManager {
    private static AppManager instance;
    private Context context;
    private AppManager(Context context) {
        this.context = context;
    }
    public static AppManager getInstance(Context context) {
        if (instance != null) {
            instance = new AppManager(context);
        }
        return instance;
    }
}

這樣不管傳入什麼Context最終將使用Application的Context,而單例的生命周期和應用的一樣長,這樣就防止瞭內存泄漏。???

2、非靜態內部類創建靜態實例造成的內存泄漏【已無】

例如,有時候我們可能會在啟動頻繁的Activity中,為瞭避免重復創建相同的數據資源,可能會出現如下寫法:

public class MainActivity extends AppCompatActivity {
 
    private static TestResource mResource = null;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if(mResource == null){
            mResource = new TestResource();
        }
        //...
    }
    
    class TestResource {
    //...
    }
}

這樣在Activity內部創建瞭一個非靜態內部類的單例,每次啟動Activity時都會使用該單例的數據。雖然這樣避免瞭資源的重復創建,但是這種寫法卻會造成內存泄漏。因為非靜態內部類默認會持有外部類的引用,而該非靜態內部類又創建瞭一個靜態的實例,該實例的生命周期和應用的一樣長,這就導致瞭該靜態實例一直會持有該Activity的引用,從而導致Activity的內存資源不能被正常回收。
解決方法:將該內部類設為靜態內部類或將該內部類抽取出來封裝成一個單例,如果需要使用Context,就使用Application的Context。

3、Handler造成的內存泄漏

示例:創建匿名內部類的靜態對象

public class MainActivity extends AppCompatActivity {
 
    private final Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            // ...
        }
    };
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
 
        new Thread(new Runnable() {
            @Override
            public void run() {
                // ...
                handler.sendEmptyMessage(0x123);
            }
        });
    }
}

1、從Android的角度
當Android應用程序啟動時,該應用程序的主線程會自動創建一個Looper對象和與之關聯的MessageQueue。當主線程中實例化一個Handler對象後,它就會自動與主線程Looper的MessageQueue關聯起來。所有發送到MessageQueue的Messag都會持有Handler的引用,所以Looper會據此回調Handle的handleMessage()方法來處理消息。隻要MessageQueue中有未處理的Message,Looper就會不斷的從中取出並交給Handler處理。另外,主線程的Looper對象會伴隨該應用程序的整個生命周期。
2、 Java角度
在Java中,非靜態內部類和匿名類內部類都會潛在持有它們所屬的外部類的引用,但是靜態內部類卻不會。

對上述的示例進行分析,當MainActivity結束時,未處理的消息持有handler的引用,而handler又持有它所屬的外部類也就是MainActivity的引用。這條引用關系會一直保持直到消息得到處理,這樣阻止瞭MainActivity被垃圾回收器回收,從而造成瞭內存泄漏。
解決方法:將Handler類獨立出來或者使用靜態內部類,這樣便可以避免內存泄漏。

4、線程造成的內存泄漏

示例:AsyncTask和Runnable

public class MainActivity extends AppCompatActivity {
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
 
        new Thread(new MyRunnable()).start();
        new MyAsyncTask(this).execute();
    }
 
    class MyAsyncTask extends AsyncTask<Void, Void, Void> {
 
        // ...
 
        public MyAsyncTask(Context context) {
            // ...
        }
 
        @Override
        protected Void doInBackground(Void... params) {
            // ...
            return null;
        }
 
        @Override
        protected void onPostExecute(Void aVoid) {
            // ...
        }
    }
 
    class MyRunnable implements Runnable {
        @Override
        public void run() {
            // ...
        }
    }
}

AsyncTask和Runnable都使用瞭匿名內部類,那麼它們將持有其所在Activity的隱式引用。如果任務在Activity銷毀之前還未完成,那麼將導致Activity的內存資源無法被回收,從而造成內存泄漏。
解決方法:將AsyncTask和Runnable類獨立出來或者使用靜態內部類,這樣便可以避免內存泄漏。

5、資源未關閉造成的內存泄漏

對於使用瞭BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等資源,應該在Activity銷毀時及時關閉或者註銷,否則這些資源將不會被回收,從而造成內存泄漏。

1)比如在Activity中register瞭一個BraodcastReceiver,但在Activity結束後沒有unregister該BraodcastReceiver。
2)資源性對象比如Cursor,Stream、File文件等往往都用瞭一些緩沖,我們在不使用的時候,應該及時關閉它們,以便它們的緩沖及時回收內存。它們的緩沖不僅存在於 java虛擬機內,還存在於java虛擬機外。如果我們僅僅是把它的引用設置為null,而不關閉它們,往往會造成內存泄漏。
3)對於資源性對象在不使用的時候,應該調用它的close()函數將其關閉掉,然後再設置為null。在我們的程序退出時一定要確保我們的資源性對象已經關閉。
4)Bitmap對象不在使用時調用recycle()釋放內存。2.3以後的bitmap應該是不需要手動recycle瞭,內存已經在java層瞭。

6、使用ListView時造成的內存泄漏

初始時ListView會從BaseAdapter中根據當前的屏幕佈局實例化一定數量的View對象,同時ListView會將這些View對象緩存起來。當向上滾動ListView時,原先位於最上面的Item的View對象會被回收,然後被用來構造新出現在下面的Item。這個構造過程就是由getView()方法完成的,getView()的第二個形參convertView就是被緩存起來的Item的View對象(初始化時緩存中沒有View對象則convertView是null)。

構造Adapter時,沒有使用緩存的convertView。
解決方法:在構造Adapter時,使用緩存的convertView。

7、集合容器中的內存泄露

我們通常把一些對象的引用加入到瞭集合容器(比如ArrayList)中,當我們不需要該對象時,並沒有把它的引用從集合中清理掉,這樣這個集合就會越來越大。如果這個集合是static的話,那情況就更嚴重瞭。
解決方法:在退出程序之前,將集合裡的東西clear,然後置為null,再退出程序。

8、WebView造成的泄露

當我們不要使用WebView對象時,應該調用它的destory()函數來銷毀它,並釋放其占用的內存,否則其長期占用的內存也不能被回收,從而造成內存泄露。
解決方法:為WebView另外開啟一個進程,通過AIDL與主線程進行通信,WebView所在的進程可以根據業務的需要選擇合適的時機進行銷毀,從而達到內存的完整釋放。

如何避免內存泄漏?

1、在涉及使用Context時,對於生命周期比Activity長的對象應該使用Application的Context。凡是使用Context優先考慮Application的Context,當然它並不是萬能的,對於有些地方則必須使用Activity的Context。對於Application,Service,Activity三者的Context的應用場景如下:

其中,NO1表示Application和Service可以啟動一個Activity,不過需要創建一個新的task任務隊列。而對於Dialog而言,隻有在Activity中才能創建。除此之外三者都可以使用。

2、對於需要在靜態內部類中使用非靜態外部成員變量(如:Context、View ),可以在靜態內部類中使用弱引用來引用外部類的變量來避免內存泄漏。
3、對於不再需要使用的對象,顯示的將其賦值為null,比如使用完Bitmap後先調用recycle(),再賦為null。
4、保持對對象生命周期的敏感,特別註意單例、靜態對象、全局性集合等的生命周期。
5、對於生命周期比Activity長的內部類對象,並且內部類中使用瞭外部類的成員變量,可以這樣做避免內存泄漏:
1)將內部類改為靜態內部類
2)靜態內部類中使用弱引用來引用外部類的成員變量

總結

到此這篇關於Java中的內存泄露問題和解決辦法的文章就介紹到這瞭,更多相關Java內存泄露問題內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: