Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android內存洩露

Android內存洩露

編輯:關於Android編程

本篇博客主要是記錄一下Android內存洩露相關的問題。網上有很多介紹內存洩露的文章,自己摘取了一些比較有價值的內容,同時增加了一些自己的理解。

在分析內存洩露前,我們必須先對內存洩露的定義有所了解。
簡單來講,Android對內存洩露的定義,與Java中的定義基本一致,即:正常情況下,當一個對象已經不需要再被使用時,它占用的內存就能夠被系統回收。如果一個本該被回收的無用對象,由於被其它有效對象引用,使得對應的內存不能被系統回收,就稱之為內存洩漏。

我們知道Android系統為每個應用分配的內存有限,當一個應用中產生的內存洩漏比較多時,就難免會導致應用所需要的內存超過系統分配的極限,於是就造成應用出現了OOM錯誤。因此,每個開發人員有必要對內存洩露的原理、出問題的場景及分析工具有一定的了解。

一、Java內存分配策略
Java 程序運行時的內存分配策略有三種,分別是靜態分配、棧式分配和堆式分配。
對應的,三種分配策略使用的內存空間分別是靜態存儲區、棧區和堆區。
其中:
1、靜態存儲區:主要存放靜態數據和常量。
這部分內存在程序編譯時就已經分配好,並且在程序整個運行期間都存在。

2、棧區 :當方法被執行時,方法體內的局部變量都在棧上創建,並在方法執行結束時,自動釋放其持有的內存。
由於棧內存分配相關的運算,內置於處理器的指令集中,因此執行效率很高,不過其分配的內存容量有限。

3、堆區 : 主要用於存儲動態分配的對象。
這部分內存在不使用時,將會由 Java 的垃圾回收器來負責回收。

舉例來說:

public class Example {
    //靜態存儲區
    static int e1 = 0;

    //堆區
    int e2 = 1;
    Example e3 = new Example();

    void method() {
        //棧區
        int e4 = 2;
        Example e5 = new Example();
    }
}

如上面代碼所示,e1作為靜態變量,是與Example這個類關聯的,將被分配到靜態存儲區。

e2和e3均是一個具體對象的成員變量,由於對象必須被動態創建出來,因此e2和e3均將被分配到堆區。
即類中定義的非靜態成員變量全部存儲於堆中,包括基本數據類型、引用和引用指向的對象實體。

對於一個具體的方法來說,如代碼中的method,當方法執行時,其內部的臨時變量e4、e5均分配在棧區;
當方法執行完畢後,e4和e5的內存均會被自動釋放。
這裡需要注意的是,e5分配在棧區,但其指向的對象是分配在堆區的。
由此可以看出,局部變量的基本數據類型和引用存儲於棧區,引用指向的對象實體存儲於堆區。

二、Java內存釋放策略
1、原理
Java的內存釋放策略是由GC(Garbage Collection)機制決定和執行的,主要針對的是堆區內存。
GC為了能夠准確及時地釋放對象,必須監控每一個對象的運行狀態,包括對象的申請、引用、被引用、賦值等。

關於GC釋放內存的原理,參考了一些資料,個人覺得一種比較好的理解方式是:
將堆區對象考慮為有向圖的頂點,將引用關系考慮為圖的有向邊,有向邊從引用者指向被引對象。
將每個線程對象作為一個圖的起始頂點,從起始頂點可達的對象都是有效對象,不會被GC回收;
如果某個對象從起始頂點出發不可達,那麼這個對象就可以被認為是無效的,可以被 GC 回收。

對於程序運行的每一個時刻,都可以用一個有向圖表示JVM的內存分配情況。
舉例來說,對於下面的代碼:

public class Solution {
    public static void main(String[] args) {
        Object o1 = new Object();
        Object o2 = new Test();
        o2 = o1;
        //運行到此處,看一下對應的GC有向圖
        .................
    }
}

public class Test {
    private Object o3;
    Test() {
        o3 = new Object();
    }
}

程序運行到注釋行時,對應的GC有向圖大致如下:

main函數所在進程為有向圖的根節點。
當函數運行到注釋行時,沒有從根節點到Test對象和Obj 3的路徑,
因此Test和Obj 3對象均可以被GC回收。
注意對象能否被回收的依據是,是否存在從根節點到該對象的路徑,
因此雖然Obj 3被引用了,但依然會被回收。

由上述例子可以看出,Java的GC機制使用有向圖的方式進行內存管理,可以消除引用循環的問題。
例如有三個對象相互引用,只要它們對根進程而言是不可達的,那麼GC也可以對它們進行回收。
GC的這種內存管理方式的優點是精度很高,但是效率較低。
另外一種常用的內存管理技術是使用計數器,它與有向圖相比精度較低(很難處理循環引用的問題),但執行效率較高。

最後需要提一點的是,對於程序員來說,GC對應的操作基本是透明的。
雖然可以主動調用幾個GC相關的函數,例如System.gc()等,但是根據Java語言規范定義,
這些函數並不保證GC線程一定會進行實際的工作。
這是因為不同的JVM可能使用不同的算法管理GC,例如:
有的JVM檢測到內存使用量到達門限時,才調度GC線程進行工作;
有的JVM是定時調度GC線程進行工作等。

2、Java內存洩露的例子
結合Java的GC機制,我們知道了,對於Java中分配在堆內存的對象而言:
如果某個對象是可達的,即在有向圖中,存在從根節點到這些對象的路徑;
同時這個對象是無用的,即程序以後不會再使用這個對象;
那麼該對象就被判定為Java中的內存洩漏。

關於Java內存洩露的例子,可以參考下面的代碼:

public class Example {
    private static ArrayList

類似上面的例子,如果集合類是全局性的變量,同時沒有相應的刪除機制,則很可能導致集合所占用的內存只增不減。

三、Android中的內存洩露舉例
接下來我們看看Android中一些內存洩露的例子。

1、靜態單例對象引入的洩露
靜態對象生命周期的長度與整個應用一致,
如果靜態對象持有了一個生命周期較短的對象,
例如Activity等,那麼就會導致內存洩露。

這種錯誤經常出現在使用單例對象的場景中,例如:

public class SingleInstance {
    private final static Object LOCK = new Object();

    //單例模式需要靜態對象
    private static SingleInstance singleInstance;

    //靜態對象持有Context就可能導致內存洩露
    private Context mContext;

    public static SingleInstance getInstance(Context context) {
        synchronized (LOCK) {
            if (singleInstance == null) {
                return new SingleInstance(context);
            }
            return singleInstance;
        }
    }

    private SingleInstance(Context context) {
        mContext = context;
    }
}

如上面的代碼所示:
如果獲取單例模式時傳的是Application的Context,
由於Application的生命周期就是整個應用的生命周期,
即Context與靜態對象的生命周期一致,沒有任何問題;

如果傳入的是 Activity 等的 Context,那麼當這個 Context 所對應的 Activity 退出時,
由於該 Context 的引用被靜態單例對象所持有,而單例對象將持續到應用結束,
於是即使當前 Activity 退出,它的內存也不會被回收,就造成了內存洩漏。

由此可以看出,在Android中盡量不要讓靜態對象持有Context。
如果靜態對象一定要持有Context,就讓它持有Application Context,
即上面代碼需要更改為:

public class SingleInstance {
    private final static Object LOCK = new Object();

    //Android Studio的靜態代碼檢查,會提示不要將Context類置於靜態引用中,可能會導致內存洩露
    private static SingleInstance singleInstance;

    private Context mContext;

    public static SingleInstance getInstance(Context context) {
        synchronized (LOCK) {
            if (singleInstance == null) {
                return new SingleInstance(context);
            }
            return singleInstance;
        }
    }

    private SingleInstance(Context context) {
        //若實在需要用,就獲取Application Context
        mContext = context.getApplicationContext();
    }
}

不過Application Context也不是萬能的,有些場景下Application Context是無法使用的,例如創建一個Dialog。
關於Application、Activity和Service三者的Context應用場景,自己也沒有總結過,
就截一下參考資料中的圖吧,有機會再深入研究一下:

圖中,NO1表示 Application 和 Service 可以啟動一個 Activity,不過需要創建一個新的 task 任務隊列。

2、非靜態內部類引入的洩露
如下代碼所示,在MainActivity的onCreate函數中,創建了一個非靜態內部類對象,
該對象被Activity中的一個靜態對像引用。

public class MainActivity extends AppCompatActivity {
    private static Resource mResource = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if (mResource == null) {
            mResource = new Resource();
        }
    }

    private class Resource {
        //........
    }
}

在上述代碼對應的場景中,由於非靜態內部類默認會持有外部類的引用,
而內部類的一個實例又被一個靜態對象持有,於是最終導致外部類Activity被一個靜態對象持有。、
正如前文提及的,由於靜態對象一直存在,於是Activity退出時,對應的內存也沒法被GC機制回收。

這種問題的解決方案就是,將非靜態內部類變為靜態內部類,或抽取成一個單獨的類。

3、自定義Handler引入的洩露
非靜態內部類引入內存洩漏的場景中,比較典型的就是自定義Handler引入的內存洩露:

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //這種方式獲取Handler,實際上綁定的是sThreadLocal中的Looper
        Handler handler = new MayLeakHandler();

        //延遲處理一個消息
        //這裡匿名內部類其實也會持有外部類的引用
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                //.......
            }
        }, 6000);

        //Activity界面關閉,但其內存還是將被Handler對應的靜態線程持有
        finish();
    }

    //定義一個非靜態內部類
    private class MayLeakHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            //.............
        }
    }
}

上面代碼產生內存洩露的原因是:
MayLeakHandler是一個非靜態內部類,持有對Activity的引用。
當Activity退出時,MayLeakHandler仍被有效對象引用,
於是Activity對應的內存也無法被釋放。

為了比較好的理解這個問題,我們看看Handler涉及到的一些源碼。

當創建Handler時,最終調用的源碼片段如下:

public Handler(Callback callback, boolean async) {
    ............
    //調用sThreadLocal.get(),即從應用主線程獲取Looper
    mLooper = Looper.myLooper();
    ............
    //獲取主線程的MessageQueue
    mQueue = mLooper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}

當Handler發送消息或Runnable對象時,最終將調用到如下源碼:

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    //this指handle,即Msg將持有handler
    msg.target = this;
    ..........
    //Msg被加入到MessageQueue中,被MessageQueue持有
    return queue.enqueueMessage(msg, uptimeMillis);
}

根據上面的源碼可以看出,當定義一個非靜態內部類Handler時,
該Handler將被應用主線程的MessageQueue持有;
而Handler又持有了Activity的引用,於是即使Activity界面結束,
若Msg被有被處理掉,MessageQueue將一直持有Activity導致內存洩露。

對於上面那種使用Handler的方式,通常的修改方式是:

public class MainActivity extends AppCompatActivity {
    private MayLeakHandler mHandler = new MayLeakHandler(this);

    //Runnable也必須變成靜態的,否則也會內存洩漏
    private static Runnable mRunnable = new Runnable() {
        @Override
        public void run() {
            //.......
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mHandler.postDelayed(mRunnable, 6000);

        finish();
    }

    private static class MayLeakHandler extends Handler {
        //如果MayLeakHandler需要訪問Activity中的變量,就持有Activity的弱引用
        //這樣垃圾回收時,就可以清除Activity的內存
        private WeakReference mActivity;

        MayLeakHandler(Activity activity) {
            super();
            mActivity = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            if (mActivity.get() != null) {
                //.............
            }
        }
    }

    @Override
    protected void onDestroy() {
        //最後根據需要需要,在Activity的onDestroy中清除mHandler處理的Message和Runnable
        //也可以調用其它接口單獨清理msg或runnable
        //對於這個例子,Runnable是靜態的,所以不需要
        //但其它情況msg和runnable可能會持有Activity,所以需要清理
        mHandler.removeCallbacksAndMessages(null);
        super.onDestroy();
    }
}

4、匿名內部類引入的洩露
匿名內部類也會持有外部類的引用,
因此與非靜態內部類一樣,也有可能導致內存洩露,
上面例子中初始定義的匿名Runnable,就會導致這個問題。

比較一般的場景是,如果匿名內部類被異步線程持有,
當異步線程與外部類的生命周期不一致時,就會導致內存洩露。

舉例如下:

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                //.........
            }
        };

        //假設workThread是一個長期運行的HandlerThread
        WorkThread workThread = WorkThread.getWorkThread();
        Handler handler = new Handler(workThread.getLooper());
        handler.post(runnable);
    }
}

上面的代碼中,定義了一個匿名內部類runnable,該runable對象持有對Activity的引用。
將該runnable對象遞交給WorkThread處理時,workThread就會持有該runable對象的引用,進而持有Activity對象。
如果workThread之前在進行某個耗時操作,那麼可能Activity結束時,runable對象還未執行完畢,
於是Activity對應的內存沒有及時釋放,導致內存洩露。

這種類型的問題的解決方法,可能只有將runable寫成靜態類或單獨抽取成一個獨立的類。

5、線程相關的內存洩漏
在界面中使用線程對象,稍不注意也會造成內存洩露:

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        leakOne();
    }

    private void leakOne() {
        new Thread() {
            @Override
            public void run() {
                while (true) {
                    SystemClock.sleep(1000);
                }
            }
        }.start();
    }
}

很明顯,這個問題與匿名內部類引起的內存洩露一樣,由於Thread持有對Activity的引用,
同時Thread一直在運行,因此當Activity結束時,對應內存也不會被釋放,導致內存洩露的放生。

現在,我們修改一下代碼:

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        leakTwo();
    }

    private void leakTwo() {
        new LeakThread().start();
    }

    private static class LeakThread extends Thread{
        @Override
        public void run() {
            while (true) {
                SystemClock.sleep(1000);
            }
        }
    }
}

可以看到,現在Thread變成了一個靜態內部類,不再持有對Activity的引用,
因此Activity退出後,對應的內存可以被釋放掉。

然而,這段代碼還是有問題。
Activity每次創建時,均會創建一個新的永不結束的Thread。
JVM會持有每個運行Thread的引用,因此Activity創建出的Thread將不會被釋放掉。
於是,不斷的關閉打開Activity,將導致JVM持有的Thread越來越多。

因此上述代碼需要修改為:

public class MainActivity extends AppCompatActivity {
    private LeakThread mLeakThread;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        leakThree();
    }

    private void leakThree() {
        mLeakThread = new LeakThread();
        mLeakThread.start();
    }

    private static class LeakThread extends Thread{
        private boolean mRunning = false;

        @Override
        public void run() {
            mRunning = true;
            while (mRunning) {
                SystemClock.sleep(1000);
            }
        }

        void close() {
            mRunning = false;
        }
    }

    @Override
    protected void onDestroy() {
        mLeakThread.close();
        super.onDestroy();
    }
}

修改比較簡單,就是在Activity結束時,主動停止Thread。

6、資源未關閉造成的內存洩漏
解決最後這一類的內存洩露,主要就是要注意編程細節了。

使用BroadcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等資源時,
應該在不再使用時,及時關閉或者注銷。

四、總結
對Android中的內存洩露就先總結到這裡了。
如何避免內存洩露,在上述例子中已經有對應的解決方案了,此處就不做贅述。
總之,代碼看到多寫的多,自然會養成良好的編程習慣,死記硬背一些規則,效率肯定比較低。

最後提一下檢測內存洩露的工具,MAT有很多的資料,此處不做說明了。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved