Java記憶體問題 及 LeakCanary 原理分析

常興E站發表於2018-03-26

前些天,有人問到 “開發過程中常見的記憶體洩漏都有哪些?”,一時脫口而出:靜態的物件中(包括單例)持有一個生命週期較短的引用時,或內部類的子程式碼塊物件的生命週期超過了外面程式碼的生命週期(如非靜態內部類,執行緒),會導致這個短生命週期的物件記憶體洩漏。總之就是一個物件的生命週期結束(不再使用該物件)後,依然被某些物件所持有該物件強引用的場景就是記憶體洩漏。

這樣回答很明顯並不是問答人想要的都有哪些場景,所以這裡抽時間整理了下記憶體相關的知識點,及LeakCanary工具的原理分析。

Java記憶體問題 及 LeakCanary 原理分析

在安卓等其他移動平臺上,記憶體問題顯得特別重要,想要做到虛擬機器記憶體的高效利用,及記憶體問題的快速定位,瞭解下虛擬機器記憶體模組及管理相關知識是很有必要的,這篇文章將從最基礎的知識分析,記憶體問題的產生地方、原因、解決方案等原理。

一、執行時記憶體區域

執行時資料區

這裡以Java虛擬機器為例,將執行時記憶體區分為不同的區域,每個區域承擔著不同的功能。

方法區 使用者儲存已被虛擬機器載入的類資訊,常量,靜態常量,即時編譯器編譯後的程式碼等資料。異常狀態 OutOfMemoryError,其中包含常量池和使用者存放編譯器生成的各種字面量和符號引用。

是JVM所管理的記憶體中最大的一塊。唯一目的就是存放例項物件,幾乎所有的物件例項都在這裡分配。Java堆是垃圾收集器管理的主要區域,因此很多時候也被稱為“GC堆”。異常狀態 OutOfMemoryError。

虛擬機器棧 描述的是java方法執行的記憶體模型,每個方法在執行時都會建立一個棧幀,使用者儲存區域性變數表,運算元棧,動態連線,方法出口等資訊。每一個方法從呼叫直至完成的過程,就對應著一個棧幀在虛擬機器棧中入棧到出棧的過程。 對這個區域定義了兩種異常狀態 OutOfMemoryError、StackOverflowError。

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

程式計數器 一塊較小的記憶體,當前執行緒所執行的位元組碼的行號指示器。位元組碼直譯器工作時,就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令。

記憶體模型

Java記憶體模型規定了所有的變數都儲存在主記憶體中。每條執行緒中還有自己的工作記憶體,執行緒的工作記憶體中儲存了被該執行緒所使用到的變數,這些變數是從主記憶體中拷貝而來。執行緒對變數的所有操作(讀,寫)都必須在工作記憶體中進行。不同執行緒之間也無法直接訪問對方工作記憶體中的變數,執行緒間變數值的傳遞均需要通過主記憶體來完成。

為了保證記憶體可見性,常常利用volatile關鍵子特性來保證變數的可見性(並不能保證併發時原子性)。

二、記憶體如何回收

記憶體的分配

一個物件從被建立到回收,主要經歷階段有 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種。

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

2、軟引用,在系統將要發生記憶體溢位異常之前,將會把這些物件列進回收範圍進行二次回收。如果這次回收還沒有足夠的記憶體,才會丟擲記憶體溢位異常。SoftReference表示軟引用。

3、弱引用,只要有GC,無論當前記憶體是否足夠,都會回收掉被弱引用關聯的物件。WeakReference表示弱引用。

4、虛引用,這個引用存在的唯一目的就是在這個物件被收集器回收時收到一個系統通知,被虛引用關聯的物件,和其生存時間完全沒關係。PhantomReference表示虛引用,需要搭配ReferenceQueue使用,檢測物件回收情況。

關於JVM記憶體管理的一些建議

1、儘可能的手動將無用物件置為null,加快記憶體回收。 2、可考慮物件池技術生成可重用的物件,較少物件的生成。 3、合理利用四種引用。

三、記憶體洩漏

持有一個生命週期較短的引用時或內部的子模組物件的生命週期超過了外面模組的生命週期,即本該被回收的物件不能被回收而停留在堆記憶體中,這就產生了記憶體洩漏。

記憶體洩漏是造成應用程式OOM的主要原因之一,尤其在像安卓這樣的移動平臺,難免會導致應用所需要的記憶體超過系統分配的記憶體限額,這就造成了記憶體溢位Error。

安卓平臺常見的記憶體洩漏

1、靜態成員變數持有外部(短週期臨時)物件引用。 如單例類(類內部靜態屬性)持有一個activity(或其他短週期物件)引用時,導致被持有的物件記憶體無法釋放。

2、內部類。當內部類與外部類生命週期不一致時,就會造成記憶體洩漏。如非靜態內部類建立靜態例項、Activity中的Handler或Thread等。

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

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

5、方法塊內不使用的物件,沒有及時置空。

四、如何檢測記憶體洩漏

Android Studio供了許多對App效能分析的工具,可以方便分析App效能。我們可以使用Memory Monitor和Heap Dump來觀察記憶體的使用情況、使用Allocation Tracker來跟蹤記憶體分配的情況,也可以通過這些工具來找到疑似發生記憶體洩漏的位置。

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

然而MAT工具分析記憶體問題並不是一件容易的事情,需要一定的經驗區做引用鏈的分析,需要一定的門檻。 隨著安卓技術生態的發展,LeakCanary 開源專案誕生了,只要幾行程式碼引入目標專案,就可以自動分析hpof檔案,把記憶體洩漏的地方展示出來。

五、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原始方法

六、總結

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

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


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

相關文章