談談內存泄露檢測的工具的使用、原理與未來 前言 Android系統的內存泄露檢測 未來方案探討

前言

     先回顧下內存泄露的定義:在計算機運行過程中,如果有對象超出了預期的生命週期繼續存活在內存中,導致這部分內存不能正常地回收和重新利用,我們就說發生了內存泄露。
     每個對象被創造出來都是有目的的,它爲了完成某個任務而生,當完成它的任務後,對象就會跟任務鏈上的其他對象斷開聯繫,等待最後的終結-銷燬。因此我們可以根據這個對象和其他對象的鏈接情況來判斷此對象是否超過它自己的生命週期。目前有兩種方案:
      1)引用計數法
      2)可達性分析
前者是根據是否被其他對象引用,後者是根據是否被重要對象(GC ROOT)間接或者直接引用,他們都是通過聯接情況來判斷對象是否應該被銷燬。
      沒有聯接的對象應該銷燬,但有聯接的對象可能也是需要銷燬的,所以我們根據聯接判斷,會出現“關係戶”逃脫生死審判,從而產生內存泄露。
     當系統無能爲力的時候,那麼程序員便要站出來了,他們需要每個對象進行審查,弄清楚這個對象什麼情況下什麼時候應該被銷燬,然後在這個時刻去詳細觀察內存,看看這個時候它是否真的已經被銷燬,如果沒有就是發生了內存泄露,這就是檢測內存泄露的基本思路。
     本文就Android系統中內存檢測的常見手段進行分析,在分析過程中,請思考如下問題:
     1)每種檢測手段是否檢測了所有對象,重點檢測哪些對象,爲什麼呢;
     2)每種檢測手段定位內存泄露的方法;
     3)檢測手段未來如何發展。

Android系統的內存泄露檢測

     現如今Android系統常見的內存泄露檢測手段(工具)有三個:
     1 Memory Profiler
     2 Memory Analyzer
     3 LeakCanary
接下來就主要介紹每種工具如何使用以及他們的原理。

Memory Profiler

profile: 剖面、 側面,Memory Profiler :內存剖析者,它是 Android Profiler中重要組件,可幫助分析應用卡頓、內存泄漏等原因。

Memory Profiler使用方法

一、找到Android Profiler
二、連接上手機或者打開模擬器
三、選擇Profiler裏面的Memory

可以看到Memory Profiler是利用實時圖表記錄應用內存使用情況的。用戶的每一項操作(如打開新頁面)都會伴隨着進程中某些對象的生成和銷燬,從而導致進程佔用的內存發生變化,如果這個變化與預期的內存變化不一致,我們就能猜測到可能發生了內存泄露。
    這裏就這圖中主要按鈕功能進行簡單介紹:
    1號按鈕:這個帶有垃圾桶圖標的按鈕是強制垃圾回收(GC)按鈕
    2號按鈕:這個帶有一個向下箭頭的按鈕是堆轉儲按鈕,就是把當前內存堆中的對象全部傾倒(dump)出來,放在手機SD卡等存儲設備中,形成一個.hprof文件,通過查看這個文件,我們可以看到內存中有哪些對象。
    3號按鈕: Record按鈕 或者叫 Allocation Tracking按鈕,這個按鈕可以記錄一段時間內存分配情況。但是注意這個只有在Android 7.1以下版本可以使用。爲了在分析時提高應用性能,Memory Profiler 在默認情況下會定期對內存分配進行採樣。在運行 API 級別 26 或更高級別的設備上進行測試時,您可以使用 Allocation Tracking 下拉菜單來更改此行爲。可用選項如下:Full:捕獲內存中的所有對象分配。這是 Android Studio 3.2 及更低版本中的默認行爲。如果您有一個分配了大量對象的應用,則可能會在分析時觀察到應用的運行速度明顯減慢。Sampled:定期對內存中的對象分配進行採樣。這是默認選項,在分析時對應用性能的影響較小。在短時間內分配大量對象的應用仍可能會表現出明顯的速度減慢。None:停止跟蹤應用的內存分配。
    4號按鈕: 暫停或者繼續抓取內存快照。
    5號線:某一個時刻點應用佔用內存情況。

四、Memory Profiler的使用

    從上面的Profiler主頁介紹可以看出來Memory Profiler以實時圖標形式展示內存使用情況,提供強制GC和堆轉儲的能力,在低版本手機上還可以跟蹤內存分配情況。利用這些能力我們可以初步定位內存泄露,以及發現一些簡單的內存泄露,具體使用如下:
     1)對於每一個應用來說,內存佔多少是有一定的規律的,比如打開一個頁面內存佔用就會增加,打開一張圖片內存佔用也會增加,退出一個頁面內存佔用就應該減少等,我們可以根據這些預期內存佔用曲線和實際內存佔用曲線是否一致來判斷是否發生了內存泄露。比如我們反覆打開關閉一個Activity,但是發現實際內存佔用曲線是一直異常上升,便可以初步判斷髮生了內存泄露,接下來就要重點關注這個Activity。
     2)在特定時刻,先GC,再Dump heap就可以獲取當前進程內存堆情況,我們可以看內存中對象是否和我們預期一樣,比如我們用filter 篩選出我們重點關注的Activity,如下圖:


點擊LeakDemoActivity(我們自己製造的內存泄露Activity),右側出現它的實例,我們發現它有四個實例,再點擊實例發現每個實例的引用鏈,在右下角引用鏈LeakDemoActivity,可以發現這個實例是被一個ArrayList鎖定的,我們直接點擊LeakDemoActivity去尋找列表,然後發現:

public class LeakDemoActivity extends BaseActivity {
    public final static ArrayList<Context> mContextList=new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_leak_demo);
        mContextList.add(this);
    }
}

很顯然問題就在這個mContextList。

Memory Profiler監測內存泄露原理

1:每隔一段時間對目標進程進行採樣形成內存佔用的實時圖表。
這裏存在一個問題,如何獲取當前進程內存使用情況呢?
答:主流的JVM都提供了一套API-JVMTI(JVM Tool Interface),利用這套API我們可以獲取進程很多監控數據,包括內存使用情況,cpu使用情況等。Memory Profiler命令會使進程啓動時候同時啓動一個伴生的庫(運行在同一個進程內),這個庫啓動後會調用JVMT去採樣進程數據,然後緩存-發送給我們的觀測者-android Memory Profiler。

2 手動dump內存某一個時刻點的具體分配情況,來進一步分析,一般有三種主要分析方法:
     A 通過熟悉業務流程去理清某個對象的生命週期,然後可以通過內存快照看特定時刻這個對象是否和預期一樣生或者被銷燬;
     B 通過對比不同時刻點內存快照,發現此段時間內新分配的對象,看是否跟對象生命週期表現一致;
     C 把dump來的“.hprof”文件放到MAT工具進行進一步的分析,比如find path to GC ROOT。

Memory Analyzer

Memory Analyzer使用方法

    MAT主要是提供對.hprof文件的靜態分析,是Memory profiler靜態分析部分的增強版,它能夠從多個維度對內存快照文件(.hprof)進行分析。下面介紹僅介紹下MAT在檢測內存泄露方面的一些基礎用法。



如上圖是MAT打開一個.hprof文件的首頁,下面的Histogram、Leak_Suspects是從不同角度對該文件進行分析得出的一個結果。這裏簡單介紹兩種用MAT尋找內存泄露的角度:
1)點擊Leak_suspects,這個是MAT自動分析的得出的結果,大致看了下這個僅僅是MAT列舉出來佔用內存比較多的類,它不一定會造成內存泄露,對我們定位內存問題具有一定的指導意義,同時Leak_suspects還給出了從系統層面的一些概述-比如線程,大的對象等。
2)點擊Histogram(直方圖),進入直方圖角度觀測內存,直方圖作用是列舉出來每個類的實例,這樣我們就可以在快照時刻看類實例的個數是否是正確的,如果不正確,我們還可以查找每個實例到GC Root的引用鏈,從而確定是否內存泄露。具體可以參考如下步驟,點擊Histogram進入直方圖主頁,搜索我們關注的對象比如MainActivity->右鍵選擇within ref->右鍵 path to GC root 就可以看到找到我們關注的對象爲啥沒有被回收的原因。

Memory Analyzer原理

    Memory Analyzer是一個文件分析工具,他可通過分析.hprof文件給出跟內存相關的諸多信息。這跟一個班主任從學生成績表中統計出全班平均分,各科平均分一樣的工作是差不多的。
    .hprof是什麼呢? 它是一種文件格式。文件格式呢, 不過是一種數據的包裝,可以使數的據存儲、 運輸、 使用更加安全、方便。說多了哈,.hprof 是我們利用Android studio dump(傾到)出來的,對,這個文件就是as接收到我們點擊命令後指揮手機系統把當前的cpu狀態,當前內存使用情況、當前進程的所有對象序列化之後存儲起來形成的一個文件。序列化是什麼呢?簡答來說序列化就是把一個對象和它當前的狀態存儲起來,同時也把跟這個對象有連接的對象序列化。
    .hprof有了內存狀態,有了cpu狀態,有了每個對象內存佔用和狀態以及它跟誰有鏈接,我們自己也可以人工統計出哪些對象佔內存多,哪些對象跟GC root對象有關聯,哪些對象可能存在內存泄露,但是一個.hprof文件包含數據非常多,人工統計何其耗時,所以MAT就出現了,它通過執行一些規則,自動幫我們進行第一步的分析,給出我們統計表格,內存泄露的對象,大大節省了我們分析的時間。

LeakCanary

LeakCanary使用方法

略,請百度,也可以去:https://github.com/square/leakcanary 找使用方法。

LeakCanary原理

我們看到LeakCanary主要做了三件事:
一 、定位內存泄露的對象--哪個Activity或者Fragment;
二、 指出內存泄露的原因--對象被GC ROOT引用的路徑;
三、 在通知欄展示該原因(略,自己百度);
其實它的原理也是包含兩部分的,首先我們看第一部分:
    如何定位內存泄露的對象?其實在本文的開始已經給出檢測特定對象內存泄露的基本原理:步驟一、弄清這個對象的生命週期,知道它再哪個時刻應該被銷燬;步驟二、 在這個時刻去觀察這個對象是否真的已經被銷燬,如果沒有就是發生了內存泄露。具體來說就是:
1 hook 特定類的生命週期, 比如Activity或者Fragment;
2 構建一個弱引用ObjectWatcher,弱引用上述類的實例;
3 當某個Activity或者Fragment onDestory()一小段時間後(5s),去檢查ObjectWatcher對應的弱引用是否還在,如果在執行一次GC,再次檢查,如果這時候依舊在,證明該Activity或者Fragment發生了內存泄露。

    如何指出內存泄露的原因呢,其實就是找到該對象的最短的Path to GC Root呢?這裏就是上面兩個工具的合體了:
1 調用JVM對應的api dump內存.hprof,這一步就相當於給內存照個照片存在手機存儲系統裏;
2 檢索.hprof文件, 找到上一步定位到泄露對象的path to GC Root。

至此,三種方案都介紹完畢 ,接下來我們探討下未來還有沒有更好的方案。

未來方案探討

    研究這些檢測工具的原理後,我們發現檢測內存泄露的關鍵在於三點:
一、知道哪些對象可能發生泄漏;
二、可以清晰知道這些對象在何時銷燬,比如有關於這個銷燬時間點的狀態標誌、監聽回調等;
三、在對象銷燬應該銷燬時刻(通過監聽、狀態標誌等確認)通過各種方案去檢查這些對象是否銷燬,如沒有,則給出Path to GC Root。

其中第三點是有通用解決方案的,所以比較難搞定的就是前兩點。

如何知道哪些對象可能發生內存泄露呢?
    已有方案就是列舉出來佔用內存比較大的對象,這種想法很直接也的確有效果,但也會有無用分析太多,比如佔用內存稍微小的對象太多沒有時間一個個分析等缺陷,這是一種事前預估的方案。
    因爲檢測內存泄露不是一次就能搞定的,也不是隻有一個app出現的問題,所以可以用一種動態長遠的眼光和整體的角度看待這個問題,如果能夠通過一種方便的方式(平臺活着集成sdk)來統計內存泄露,那麼就可以在某一個整體級別比如app級別、公司級別、平臺級別,通過統計或者機器學習給出內存泄露最可能發生的對象、對象類別,場景信息等,這是一種經歷史經驗模式。

如何定位內存泄露時刻呢?
    現在方案要麼是已經很確切的知道一些對象的生命週期,比如Activity;要麼就是看業務代碼確認生命週期,再人工分析內存快照快照確認,前一種只有有限的幾種,後一種純人工分析一次要很久。
    所以我能想到的改進莫過於 1 提供一個簡單的輸入可以把已知的生命週期錄入,然後實現自動分析 2 同樣通過統計或者機器學習方式提供一些懷疑對象類別的生命週期。

您還有什麼想法麼,評論區見。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章