Java內存問題 及 LeakCanary 原理分析

前些天,有人問到 「開發過程當中常見的內存泄漏都有哪些?」,一時脫口而出:靜態的對象中(包括單例)持有一個生命週期較短的引用時,或內部類的子代碼塊對象的生命週期超過了外面代碼的生命週期(如非靜態內部類,線程),會致使這個短生命週期的對象內存泄漏。總之就是一個對象的生命週期結束(再也不使用該對象)後,依然被某些對象所持有該對象強引用的場景就是內存泄漏。java

這樣回答很明顯並非問答人想要的都有哪些場景,因此這裏抽時間整理了下內存相關的知識點,及LeakCanary工具的原理分析。git

Java內存問題 及 LeakCanary 原理分析

在安卓等其餘移動平臺上,內存問題顯得特別重要,想要作到虛擬機內存的高效利用,及內存問題的快速定位,瞭解下虛擬機內存模塊及管理相關知識是頗有必要的,這篇文章將從最基礎的知識分析,內存問題的產生地方、緣由、解決方案等原理。github

1、運行時內存區域

運行時數據區

這裏以Java虛擬機爲例,將運行時內存區分爲不一樣的區域,每一個區域承擔着不一樣的功能。web

方法區 用戶存儲已被虛擬機加載的類信息,常量,靜態常量,即時編譯器編譯後的代碼等數據。異常狀態 OutOfMemoryError,其中包含常量池和用戶存放編譯器生成的各類字面量和符號引用。數據庫

是JVM所管理的內存中最大的一塊。惟一目的就是存放實例對象,幾乎全部的對象實例都在這裏分配。Java堆是垃圾收集器管理的主要區域,所以不少時候也被稱爲「GC堆」。異常狀態 OutOfMemoryError。bash

虛擬機棧 描述的是java方法執行的內存模型,每一個方法在執行時都會建立一個棧幀,用戶存儲局部變量表,操做數棧,動態鏈接,方法出口等信息。每個方法從調用直至完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。 對這個區域定義了兩種異常狀態 OutOfMemoryError、StackOverflowError。併發

本地方法棧 虛擬機棧爲虛擬機執行java方法,而本地方法棧爲虛擬機使用到的Native方法服務。異常狀態StackOverFlowError、OutOfMemoryError。app

程序計數器 一塊較小的內存,當前線程所執行的字節碼的行號指示器。字節碼解釋器工做時,就是經過改變這個計數器的值來選取下一條須要執行的字節碼指令。dom

內存模型

Java內存模型規定了全部的變量都存儲在主內存中。每條線程中還有本身的工做內存,線程的工做內存中保存了被該線程所使用到的變量,這些變量是從主內存中拷貝而來。線程對變量的全部操做(讀,寫)都必須在工做內存中進行。不一樣線程之間也沒法直接訪問對方工做內存中的變量,線程間變量值的傳遞均須要經過主內存來完成。函數

爲了保證內存可見性,經常利用volatile關鍵子特性來保證變量的可見性(並不能保證併發時原子性)。

2、內存如何回收

內存的分配

一個對象從被建立到回收,主要經歷階段有 1:建立階段(Created)、2: 應用階段(In Use)、3:不可見階段(Invisible)、4:不可達階段(Unreachable)、5:收集階段(Collected)、6:終結階段(、Finalized)、7:對象空間重分配階段(De-allocated)。

內存的分配實在建立階段,這個階段要先用類加載器加載目標class,當經過加載器檢測後,就開始爲新對象分配內存。對象分配內存大小在類加載完成後即可以肯定。 當初始化完成後,虛擬機還要對對象進行必要的設置,如那個類的實例,如何查找元數據、對象的GC年代等。

內存的回收(GC)

那些不可能再被任何途徑使用的對象,須要被回收,不然內存早晚都會被消耗空。

GC機制主要是經過可達性分析法,經過一系列稱爲「GC Roots」的對象做爲起始點,從這些節點向下搜索,搜索所走過的路徑稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈時,即GC Roots到對象不可達,則證實此對象是不可達的。

根據**《深刻理解Java虛擬機》**書中描述,可做爲GC Root的地方以下:

  • 虛擬機棧(棧幀中的局部變量區,也叫作局部變量表)中引用的對象。
  • 方法區中的類靜態屬性引用的對象。
  • 方法區中常量引用的對象。
  • 本地方法棧中JNI(Native方法)引用的對象。

當一個對象或幾個相互引用的對象組沒有任何引用鏈時,會被當成垃圾處理,能夠進行回收。

如何一個對象在程序中已經再也不使用,可是(強)引用仍是會被其餘對象持有,則稱爲內存泄漏。內存泄漏並不會使程序立刻異常,可是多處的未處理的內存泄漏則可能致使內存溢出,形成不可預估的後果。

引用的分類

在JDK1.2以後,爲了優化內存的利用及GC的效率,Java對引用的概念進行了擴充,將引用分爲強引用、軟引用、弱引用、虛引用4種。

一、強引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。

二、軟引用,在系統將要發生內存溢出異常以前,將會把這些對象列進回收範圍進行二次回收。若是此次回收尚未足夠的內存,纔會拋出內存溢出異常。SoftReference表示軟引用。

三、弱引用,只要有GC,不管當前內存是否足夠,都會回收掉被弱引用關聯的對象。WeakReference表示弱引用。

四、虛引用,這個引用存在的惟一目的就是在這個對象被收集器回收時收到一個系統通知,被虛引用關聯的對象,和其生存時間徹底不要緊。PhantomReference表示虛引用,須要搭配ReferenceQueue使用,檢測對象回收狀況。

關於JVM內存管理的一些建議

一、儘量的手動將無用對象置爲null,加快內存回收。 二、可考慮對象池技術生成可重用的對象,較少對象的生成。 三、合理利用四種引用。

3、內存泄漏

持有一個生命週期較短的引用時或內部的子模塊對象的生命週期超過了外面模塊的生命週期,即本該被回收的對象不能被回收而停留在堆內存中,這就產生了內存泄漏。

內存泄漏是形成應用程序OOM的主要緣由之一,尤爲在像安卓這樣的移動平臺,不免會致使應用所須要的內存超過系統分配的內存限額,這就形成了內存溢出Error。

安卓平臺常見的內存泄漏

一、靜態成員變量持有外部(短週期臨時)對象引用。 如單例類(類內部靜態屬性)持有一個activity(或其餘短週期對象)引用時,致使被持有的對象內存沒法釋放。

二、內部類。當內部類與外部類生命週期不一致時,就會形成內存泄漏。如非靜態內部類建立靜態實例、Activity中的Handler或Thread等。

三、資源沒有及時關閉。如數據庫、IO流、Bitmap、註冊的相關服務、webview、動畫等。

四、集合內部Item沒有置空。

五、方法塊內不使用的對象,沒有及時置空。

4、如何檢測內存泄漏

Android Studio供了許多對App性能分析的工具,能夠方便分析App性能。咱們可使用Memory Monitor和Heap Dump來觀察內存的使用狀況、使用Allocation Tracker來跟蹤內存分配的狀況,也能夠經過這些工具來找到疑似發生內存泄漏的位置。

堆存儲文件(hpof)可使用DDMS或者Memory Monitor來生成,輸出的文件格式爲hpof,而MAT(Memory Analysis Tool)就是來分析堆存儲文件的。

然而MAT工具分析內存問題並非一件容易的事情,須要必定的經驗區作引用鏈的分析,須要必定的門檻。 隨着安卓技術生態的發展,LeakCanary 開源項目誕生了,只要幾行代碼引入目標項目,就能夠自動分析hpof文件,把內存泄漏的地方展現出來。

5、LeakCanary原理解析

LeakCanary

A small leak will sink a great ship.

LeakCanary內存檢測工具是由squar公司開源的著名項目,這裏主要分析下源碼實現原理。

基本原理

主要是在Activity的&onDestroy方法中,手動調用 GC,而後利用ReferenceQueue+WeakReference,來判斷是否有釋放不掉的引用,而後結合dump memory的hpof文件, 用HaHa分析出泄漏地方。

源碼分析

LeakCanary集成很方便,只要幾行代碼,因此能夠從入口跟蹤代碼,分析原理

if (!LeakCanary.isInAnalyzerProcess(WeiboApplication.this)) {
                    LeakCanary.install(WeiboApplication.this);
                }
                
                public static RefWatcher install(Application application) {
                      return ((AndroidRefWatcherBuilder)refWatcher(application)
                      .listenerServiceClass(DisplayLeakService.class).excludedRefs(AndroidExcludedRefs.createAppDefaults().build()))//配置監聽器及分析數據格式
                      .buildAndInstall();
               }
複製代碼

從這裏可看出,LeakCanary會單獨開一進程,用來執行分析任務,和監放任務分開處理。

方法install中主要是構造來一個RefWatcher,

public RefWatcher buildAndInstall() {
        RefWatcher refWatcher = this.build();
        if(refWatcher != RefWatcher.DISABLED) {
            LeakCanary.enableDisplayLeakActivity(this.context);
            ActivityRefWatcher.install((Application)this.context, refWatcher);
        }

        return refWatcher;
    }
    
    public static void install(Application application, RefWatcher refWatcher) {
        (new ActivityRefWatcher(application, refWatcher)).watchActivities();
    }
    
    private final ActivityLifecycleCallbacks lifecycleCallbacks = new ActivityLifecycleCallbacks() {
        public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
        }
        public void onActivityStarted(Activity activity) {}
        public void onActivityResumed(Activity activity) {}
        public void onActivityPaused(Activity activity) {}
        public void onActivityStopped(Activity activity) { }
        public void onActivitySaveInstanceState(Activity activity, Bundle outState) {}

        public void onActivityDestroyed(Activity activity) {
            ActivityRefWatcher.this.onActivityDestroyed(activity);
        }
    };
    
    void onActivityDestroyed(Activity activity) {
        this.refWatcher.watch(activity);
    }
複製代碼

具體監聽的原理在於 Application 的registerActivityLifecycleCallbacks方法,該方法能夠對應用內全部 Activity 的生命週期作監聽, LeakCanary只監聽了Destroy方法。

在每一個Activity的OnDestroy()方法中都會回調refWatcher.watch()方法,那咱們找到的RefWatcher的實現類,看看具體作什麼。

public void watch(Object watchedReference, String referenceName) {
        if(this != DISABLED) {
            Preconditions.checkNotNull(watchedReference, "watchedReference");
            Preconditions.checkNotNull(referenceName, "referenceName");
            long watchStartNanoTime = System.nanoTime();
            String key = UUID.randomUUID().toString();//保證key的惟一性
            this.retainedKeys.add(key);
            KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, this.queue);
            this.ensureGoneAsync(watchStartNanoTime, reference);
        }
    }
    
    
  final class KeyedWeakReference extends WeakReference<Object> {
    public final String key;
    public final String name;

    KeyedWeakReference(Object referent, String key, String name, ReferenceQueue<Object> referenceQueue) {//ReferenceQueue類監聽回收狀況
        super(Preconditions.checkNotNull(referent, "referent"), (ReferenceQueue)Preconditions.checkNotNull(referenceQueue, "referenceQueue"));
        this.key = (String)Preconditions.checkNotNull(key, "key");
        this.name = (String)Preconditions.checkNotNull(name, "name");
    }
  }

    private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
        this.watchExecutor.execute(new Retryable() {
            public Result run() {
                return RefWatcher.this.ensureGone(reference, watchStartNanoTime);
            }
        });
    }
複製代碼

KeyedWeakReference是WeakReference類的子類,用了 KeyedWeakReference(referent, key, name, ReferenceQueue )的構造方法,將監聽的對象(activity)引用傳遞進來,而且New出一個ReferenceQueue來監聽GC後 的回收狀況。

如下代碼ensureGone()方法就是LeakCanary進行檢測回收的核心代碼:

Result ensureGone(KeyedWeakReference reference, long watchStartNanoTime) {
        long gcStartNanoTime = System.nanoTime();
        long watchDurationMs = TimeUnit.NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
        this.removeWeaklyReachableReferences();//先將引用嘗試從隊列中poll出來
        if(this.debuggerControl.isDebuggerAttached()) {//規避調試模式
            return Result.RETRY;
        } else if(this.gone(reference)) {//檢測是否已經回收
            return Result.DONE;
        } else {
        //若是沒有被回收,則手動GC
            this.gcTrigger.runGc();//手動GC方法
            this.removeWeaklyReachableReferences();//再次嘗試poll,檢測是否被回收
            if(!this.gone(reference)) {
    			// 尚未被回收,則dump堆信息,調起分析進程進行分析
                long startDumpHeap = System.nanoTime();
                long gcDurationMs = TimeUnit.NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
                File heapDumpFile = this.heapDumper.dumpHeap();
                if(heapDumpFile == HeapDumper.RETRY_LATER) {
                    return Result.RETRY;//須要重試
                }

                long heapDumpDurationMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
                this.heapdumpListener.analyze(new HeapDump(heapDumpFile, reference.key, reference.name, this.excludedRefs, watchDurationMs, gcDurationMs, heapDumpDurationMs));
            }

            return Result.DONE;
        }
    }

    private boolean gone(KeyedWeakReference reference) {
        return !this.retainedKeys.contains(reference.key);
    }

    private void removeWeaklyReachableReferences() {
        KeyedWeakReference ref;
        while((ref = (KeyedWeakReference)this.queue.poll()) != null) {
            this.retainedKeys.remove(ref.key);
        }
    }
複製代碼

方法ensureGone中經過檢測referenceQueue隊列中的引用狀況,來判斷回收狀況,經過手動GC來進一步確認回收狀況。 整個過程確定是個耗時卡UI的,整個過程會在WatchExecutor中執行的,那WatchExecutor又是在哪裏執行的呢?

LeakCanary已經利用Looper機制作了必定優化,利用主線程空閒的時候執行檢測任務,這裏找到WatchExecutor的實現類,研究下原理:

public final class AndroidWatchExecutor implements WatchExecutor {
    static final String LEAK_CANARY_THREAD_NAME = "LeakCanary-Heap-Dump";
    private final Handler mainHandler = new Handler(Looper.getMainLooper());
    private final Handler backgroundHandler;
    private final long initialDelayMillis;
    private final long maxBackoffFactor;

    public AndroidWatchExecutor(long initialDelayMillis) {
        HandlerThread handlerThread = new HandlerThread("LeakCanary-Heap-Dump");
        handlerThread.start();
        this.backgroundHandler = new Handler(handlerThread.getLooper());
        this.initialDelayMillis = initialDelayMillis;
        this.maxBackoffFactor = 9223372036854775807L / initialDelayMillis;
    }

    public void execute(Retryable retryable) {
        if(Looper.getMainLooper().getThread() == Thread.currentThread()) {
            this.waitForIdle(retryable, 0);//須要在主線程中檢測
        } else {
            this.postWaitForIdle(retryable, 0);//post到主線程
        }

    }

    void postWaitForIdle(final Retryable retryable, final int failedAttempts) {
        this.mainHandler.post(new Runnable() {
            public void run() {
                AndroidWatchExecutor.this.waitForIdle(retryable, failedAttempts);
            }
        });
    }

    void waitForIdle(final Retryable retryable, final int failedAttempts) {
        Looper.myQueue().addIdleHandler(new IdleHandler() {
            public boolean queueIdle() {
                AndroidWatchExecutor.this.postToBackgroundWithDelay(retryable, failedAttempts);//切換到子線程
                return false;
            }
        });
    }

    void postToBackgroundWithDelay(final Retryable retryable, final int failedAttempts) {
        long exponentialBackoffFactor = (long)Math.min(Math.pow(2.0D, (double)failedAttempts), (double)this.maxBackoffFactor);
        long delayMillis = this.initialDelayMillis * exponentialBackoffFactor;
        this.backgroundHandler.postDelayed(new Runnable() {
            public void run() {
                Result result = retryable.run();//RefWatcher.this.ensureGone(reference, watchStartNanoTime)執行
                if(result == Result.RETRY) {
                    AndroidWatchExecutor.this.postWaitForIdle(retryable, failedAttempts + 1);
                }

            }
        }, delayMillis);
    }
}
複製代碼

這裏用到了Handler相關知識,Looper中的MessageQueue有個mIdleHandlers隊列,在獲取下個要執行的Message時,若是沒有發現可執行的下個Msg,就會回調queueIdle()方法。

Message next() {
        int pendingIdleHandlerCount = -1; // -1 only during first iteration
        int nextPollTimeoutMillis = 0;
        for (;;) {
           		···
           		···//省略部分消息查找代碼
           		
           		if (msg != null) {
                    if (now < msg.when) {
                        // Next message is not ready.  Set a timeout to wake up when it is ready.
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        ···
                        
                        return msg;
                    }
                } else {
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }

           		
                // If first time idle, then get the number of idlers to run.
                // Idle handles only run if the queue is empty or if the first message
                // in the queue (possibly a barrier) is due to be handled in the future.
                if (pendingIdleHandlerCount < 0
                        && (mMessages == null || now < mMessages.when)) {
                    pendingIdleHandlerCount = mIdleHandlers.size();
                }
                if (pendingIdleHandlerCount <= 0) {
                    // No idle handlers to run.  Loop and wait some more.
                    mBlocked = true;
                    continue;
                }

                if (mPendingIdleHandlers == null) {
                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
                }
                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
            }

            // Run the idle handlers.
            // We only ever reach this code block during the first iteration.
            for (int i = 0; i < pendingIdleHandlerCount; i++) {
                final IdleHandler idler = mPendingIdleHandlers[i];
                mPendingIdleHandlers[i] = null; // release the reference to the handler

                boolean keep = false;
                try {
                    keep = idler.queueIdle();
                } catch (Throwable t) {
                    Log.wtf(TAG, "IdleHandler threw exception", t);
                }

                if (!keep) {//返回false,則從隊列移除,下次空閒不會調用。
                    synchronized (this) {
                        mIdleHandlers.remove(idler);
                    }
                }
            }

            // Reset the idle handler count to 0 so we do not run them again.
            pendingIdleHandlerCount = 0;

            // While calling an idle handler, a new message could have been delivered
            // so go back and look again for a pending message without waiting.
            nextPollTimeoutMillis = 0;
        }
    }
複製代碼

其中的MessageQueue中加入一個IdleHandler,當線程空閒時,就會去調用*queueIdle()*函數,若是返回值爲True,那麼後續空閒時會繼續的調用此函數,不然再也不調用;

知識點

1,用ActivityLifecycleCallbacks接口來檢測Activity生命週期 2,WeakReference + ReferenceQueue 來監聽對象回收狀況 3,Apolication中可經過processName判斷是不是任務執行進程 4,MessageQueue中加入一個IdleHandler來獲得主線程空閒回調 5,LeakCanary檢測只針對Activiy裏的相關對象。其餘類沒法使用,還得用MAT原始方法

6、總結

內存相關的問題基本問題回顧了下,發現技術細節越扒越多。想要獲得技術的提升,對這些技術細節的掌握是必要的,只有長時間的積累紮實的技術細節基礎,才能讓本身的技術走的更高。

基礎知識對每一個工程師發展的不一樣階段意義不一樣,理解的角度和深度也不一樣。至少本身來看,基礎知識是永遠值得學習和鞏固,來支撐技術的創新實踐。


歡迎轉載,請標明出處:常興E站 canking.win

相關文章
相關標籤/搜索