Android 內存泄漏

Android內存泄漏是一個常常要遇到的問題,程序在內存泄漏的時候很容易致使OOM的發生。那麼如何查找內存泄漏和避免內存泄漏就是須要知曉的一個問題,首先咱們須要知道一些基礎知識。java

Java的四種引用

強引用: 強引用是Java中最普通的引用,隨意建立一個對象而後在其餘的地方引用一下,就是強引用,強引用的對象Java寧願OOM也不會回收他android

軟引用: 軟引用是比強引用弱的引用,在Java gc的時候,若是軟引用所引用的對象被回收,首次gc失敗的話會繼而回收軟引用的對象,軟引用適合作緩存處理 能夠和引用隊列(ReferenceQueue)一塊兒使用,當對象被回收以後保存他的軟引用會放入引用隊列算法

弱引用: 弱引用是比軟引用更加弱的引用,當Java執行gc的時候,若是弱引用所引用的對象被回收,不管他有沒有用都會回收掉弱引用的對象,不過gc是一個比較低優先級的線程,不會那麼及時的回收掉你的對象。 能夠和引用隊列一塊兒使用,當對象被回收以後保存他的弱引用會放入引用隊列緩存

虛引用: 虛引用和沒有引用是同樣的,他必須和引用隊列一塊兒使用,當Java回收一個對象的時候,若是發現他有虛引用,會在回收對象以前將他的虛引用加入到與之關聯的引用隊列中。 能夠經過這個特性在一個對象被回收以前採起措施併發

下面是一個例子:oracle

public class Main {

    public static void main(String[] args) throws InterruptedException {
        ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
        String sw = "虛引用";
        switch (sw) {
            case "軟引用":
                Object objSoft = new Object();
                SoftReference<Object> softReference = new SoftReference<>(objSoft, referenceQueue);
                System.out.println("GC前獲取:" + softReference.get());
                objSoft = null;
                System.gc();
                Thread.sleep(1000);
                System.out.println("GC後獲取:" + softReference.get());
                System.out.println("隊列中的結果:" + referenceQueue.poll());
                break;
                /*
                 * GC前獲取:java.lang.Object@61bbe9ba
                 * GC後獲取:java.lang.Object@61bbe9ba
                 * 隊列中的結果:null
                 * */
            case "弱引用":
                Object objWeak = new Object();
                WeakReference<Object> weakReference = new WeakReference<>(objWeak, referenceQueue);
                System.out.println("GC前獲取:" + weakReference.get());
                objWeak = null;
                System.gc();
                Thread.sleep(1000);
                System.out.println("GC後獲取:" + weakReference.get());
                System.out.println("隊列中的結果:" + referenceQueue.poll());
                /*
                * GC前獲取:java.lang.Object@61bbe9ba
                * GC後獲取:null
                * 隊列中的結果:java.lang.ref.WeakReference@610455d6
                * */
                break;
            case "虛引用":
                Object objPhan = new Object();
                PhantomReference<Object> phantomReference = new PhantomReference<>(objPhan, referenceQueue);
                System.out.println("GC前獲取:" + phantomReference.get());
                objPhan = null;
                System.gc();
                //此處的區別是當objPhan的內存被gc回收以前虛引用就會被加入到ReferenceQueue隊列中,其餘的引用都爲當引用被gc掉時候,引用會加入到ReferenceQueue中
                Thread.sleep(1000);
                System.out.println("GC後獲取:" + phantomReference.get());
                System.out.println("隊列中的結果:" + referenceQueue.poll());
                /*
                * GC前獲取:java.lang.Object@61bbe9ba
                * GC後獲取:null
                * 隊列中的結果:java.lang.ref.WeakReference@610455d6
                * */
                break;
        }
    }

}

Java GC

目前oracle jdk和open jdk的虛擬機都爲Hotspot,android 爲Dalvik和Artide

曾經的GC算法:引用計數工具

簡短的說引用計數就是對每個對象的引用計算數字,若是引用就+1,不引用就-1,回收掉引用計數爲0的對象。來達到垃圾回收post

弊端:若是兩個對象都應該被回收可是他倆卻互相依賴,那麼他二者的引用永遠都不會爲0,那麼就永遠沒法回收, 沒法解決循環引用的問題ui

這個算法只在不多數的虛擬機中使用過

現代的GC算法

  • 標記回收算法(Mark and Sweep GC) :從"GC Roots"集合開始,將內存整個遍歷一次,保留全部能夠被GC Roots直接或間接引用到的對象,而剩下的對象都看成垃圾對待並回收,這個算法須要中斷進程內其它組件的執行而且可能產生內存碎片。
  • 複製算法(Copying) :將現有的內存空間分爲兩快,每次只使用其中一塊,在垃圾回收時將正在使用的內存中的存活對象複製到未被使用的內存塊中,以後,清除正在使用的內存塊中的全部對象,交換兩個內存的角色,完成垃圾回收。
  • 標記-壓縮算法(Mark-Compact) :先須要從根節點開始對全部可達對象作一次標記,但以後,它並不簡單地清理未標記的對象,而是將全部的存活對象壓縮到內存的一端。以後,清理邊界外全部的空間。這種方法既避免了碎片的產生,又不須要兩塊相同的內存空間,所以,其性價比比較高。
  • 分代 :將全部的新建對象都放入稱爲年輕代的內存區域,年輕代的特色是對象會很快回收,所以,在年輕代就選擇效率較高的複製算法。當一個對象通過幾回回收後依然存活,對象就會被放入稱爲老生代的內存空間。對於新生代適用於複製算法,而對於老年代則採起標記-壓縮算法。

以上四種算法信息引用自QQ空間團隊分享 Android GC 那點事 &version=11000003&pass_ticket=nhSGhYD4LC9FWvUPv26Y7AdIzqEDu8FTImf2AKlyrCk%3D) ,總結的特別棒

致使內存泄漏的緣由

對象在GC Root中可達,也就是他的引用不爲空,因此GC沒法回收它也就會致使內存泄漏

GC Root起點

  • 虛擬機棧中引用的對象
  • 方法區中類靜態屬性引用的對象
  • 方法區中常量引用的對象
  • JNI引用的對象

GC能夠續一秒

當一個對象在引用鏈中失S#x53BB;了引用,那麼他就真的要告別世界了嗎,其實並非,虛擬機會給他「緩刑」,每個對象有一個finalize() 方法,虛擬機是否給他緩刑取決於這個對象的這個方法是否被執行,若是這個對象的這個方法沒有被覆蓋或者這個方法被執行過一次,那麼就要「行刑」了。真的是「續一秒」

若是這個對象的finalize()方法應該被執行,那麼虛擬機會將它放在F-Queue隊列中,稍後虛擬機會自動建立一個Finalizer線程去執行這個隊列中的對象的這個方法。若是對象在finalize()中成功自救,舉個例子,把本身和一個存在的對象強引用,那麼就不會被回收,不然就真的被回收了。

可是虛擬機並不會保證Finalizer線程執行結束再進行回收,由於若是在某一個對象的finalize()方法中執行了死循環或者超級耗時的操做,虛擬機等待這個執行結束的話就會致使整個Gc崩潰了

首先注意這個方法只能被執行一次,第二次就會標記了這個方法被執行過不會再執行了,其次,這個方法不必定會被執行到,因此不要依賴finalize()去自救。這不是好的作法。

併發GC和非併發GC

Android2.3以後支持了併發的GC。

  • 非併發GC : 虛擬機在執行GC的時候進行Stop the world,也就是掛起其餘全部的線程,一般會持續上百毫秒,一次Mark,而後直接清理

  • 併發GC : 跟非併發的簡單gc來比較,通常非併發GC須要耗費上百ms的時間來進行,而併發gc僅僅須要10ms左右的時間,效率大幅度提高(數據來自:技術小黑屋大大),可是併發gc因爲須要進行重複的處理改動的對象,因此須要更多的CPU資源

二者的差異:

首先非併發GC簡單粗暴,直接掛起全部的線程,此時Java堆中確定不會有任何的添加和修改,此時去遞歸GC樹,而後標記-清理。可是這樣會形成很大的開銷,你們都等着你豈不是很沒面子= =

然而非併發的GC是一點一點來的,跟線程同步進行這樣就不會有很長時間的等待,可是你要明白一個道理,想把地掃乾淨這段時間必須沒人來踩,因此他要有掛起線程的過程。

那麼併發是怎麼實現的呢?首先有個知識點就是Jvm在分配內存的時候,有兩種方式

  • 指針碰撞:一個指針,申請一塊內存就指針挪動相應的距離,不會產生內存碎片,這要求內存是很規整的
  • 空閒列表:每次申請一塊內存給須要的對象,而後有一個列表記錄了哪些位置被申請了,下次申請的時候就不申請這個位置,這樣適用於內存不是很規整的狀況

建立對象是一個頻繁的操做,那麼咱們如何保證原子性呢?兩種方案

  • CAS(Compare and Swap)策略配上失敗重試來保證原子性
  • 每一個線程分配一個TLAB : 很簡單,每一個線程本身有本身的一塊內存,那麼分配的時候本身鎖本身的分區就好了,提升了效率

咱們用的是第二種 233

因此獲取Java堆鎖的時候,重點來了,咱們逐個線程去鎖TLAB,而不是一次全鎖住,固然提升了併發GC的效率,因此更快。可是引來的問題就是併發的問題,因此下一步要重複去修改在一個個探索時候被改的對象。也就須要更多的CPU資源。

咱們爲何要關注GC

首先咱們知道虛擬機如何去GC才能瞭解到如何讓一個對象被正確的回收,這樣纔不能內存泄漏

其次不管是併發GC仍是非併發GC都會致使掛起其餘的全部線程,那麼就會帶來程序卡頓。

ART在GC上作到了更加細粒度的控制,能夠更加流暢的GC

常見的內存泄漏案例:Handler內存泄漏

首先鋪墊一句話:非靜態的內部類和匿名類會隱式的持有外部類的引用

public class MainActivity extends AppCompatActivity {

    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            Log.d("smallSohoSolo", "Hello Handler");
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                Log.d("smallSohoSolo", "Running");
            }
        }, 1000 * 60 * 10); //10分鐘以後執行
        finish();
    }
}

這段代碼有很明顯的內存泄漏,首先Handler和Runnable都是匿名內部類的實例,他們都會持有MainActivity的引用,

  1. Handler發送的消息到了消息隊列中
  2. Activity被結束掉
  3. 這個消息中包含了Handler的引用,Handler包含了Activity的引用,並且他仍是個Runnable,也是匿名內部類,也間接包含了MainActivity引用
  4. 在Main Lopper中,當此消息被取出來,這未執行的10分鐘裏面,MainActivity無法回收
  5. 內存泄漏

有人可能會說短暫的內存泄漏又能怎樣?這是錯誤的想法,由於只要發生內存泄漏,在這段時間只要進行了大內存的操做(好比加載一個照片牆),就有風險由於這個內存泄漏形成OOM(佔用內存確定剩下的少了)

上面這個如何修改呢?

將Runnable和Handler改爲static 或者在外部定義內部使用。

其餘常見的內存泄漏

  • 靜態變量內存泄漏:使用靜態變量來引用一個事物,在不使用以後沒有下掉,那麼引用存在就會一直泄漏
  • 單例致使的內存泄漏:使用的單例中保存了不該該被一直持有的對象,那麼就會形成內存泄漏
  • 由第三方庫使用不當致使的內存泄漏:好比EventBus,Activity銷燬的時候沒有反註冊就會致使引用一直被持有沒法回收
  • 還有不少。。。他們都是由於引用沒有被清理形成的

如何查看內存泄漏

簡單粗暴 —> LeakCanary: Square出品的庫,當出現內存泄漏的時候會出現

精打細算 —> Android Studio 內存工具: 能夠Dump下來當前的內存路徑,而後分析出來哪些對象目前的狀態。很強

相關文章
相關標籤/搜索