Android 記憶體泄漏

来源:http://www.cnblogs.com/zihang814/archive/2017/09/13/7514623.html
-Advertisement-
Play Games

Android記憶體泄漏是一個經常要遇到的問題,程式在記憶體泄漏的時候很容易導致OOM的發生。那麼如何查找記憶體泄漏和避免記憶體泄漏就是需要知曉的一個問題,首先我們需要知道一些基礎知識。 Java的四種引用 強引用: 強引用是Java中最普通的引用,隨意創建一個對象然後在其他的地方引用一下,就是強引用,強引 ...


Android記憶體泄漏是一個經常要遇到的問題,程式在記憶體泄漏的時候很容易導致OOM的發生。那麼如何查找記憶體泄漏和避免記憶體泄漏就是需要知曉的一個問題,首先我們需要知道一些基礎知識。

Java的四種引用

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

軟引用: 軟引用是比強引用弱的引用,在Java gc的時候,如果軟引用所引用的對象被回收,首次gc失敗的話會繼而回收軟引用的對象,軟引用適合做緩存處理 可以和引用隊列(ReferenceQueue)一起使用,當對象被回收之後保存他的軟引用會放入引用隊列

弱引用: 弱引用是比軟引用更加弱的引用,當Java執行gc的時候,如果弱引用所引用的對象被回收,無論他有沒有用都會回收掉弱引用的對象,不過gc是一個比較低優先順序的線程,不會那麼及時的回收掉你的對象。 可以和引用隊列一起使用,當對象被回收之後保存他的弱引用會放入引用隊列

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

下麵是一個例子:

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和Art

曾經的GC演算法:引用計數

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

弊端:如果兩個對象都應該被回收但是他倆卻互相依賴,那麼他兩者的引用永遠都不會為0,那麼就永遠無法回收, 無法解決迴圈引用的問題

這個演算法只在很少數的虛擬機中使用過

現代的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下來當前的記憶體路徑,然後分析出來哪些對象目前的狀態。很強


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 不太擅長總結挺早的東西了,突然覺得都記錄下來,小demo也比較簡單,歡迎討論指正。 之前 ui的設計稿選擇框不想要預設樣式,預設樣式改起來也是太心塞,有的還改不了,所以乾脆自己寫了一個div模擬的選擇框 先看效果吧: 代碼實現不多,也都很簡單,js部分是純原聲的所以不需要引用其他框架就可以用: 先H ...
  • 在這之前,我已經分享過一個webpack的全系列,相對於webpack, gulp使用和配置起來非常的簡單. gulp是什麼? gulp 是基於 node 實現 Web 前端自動化開發的工具,利用它能夠極大的提高開發效率。在 Web 前端開發工作中有很多“重覆工作”,比如壓縮CSS/JS文件。而這些 ...
  • 本文介紹如何獲取視頻中某個時間點的數據 調用以下方法即可,特別註意,在獲取圖片時的參數單位為微秒,不是毫秒 如果錯用了毫秒會一直獲取第一幀的畫面 ...
  • UIScrollViewDelegate - (void)scrollViewDidScroll:(UIScrollView *)scrollView;//scrollview 滾動的時候調用該方法,任何 offset 值改變都會調用該方法. - (void)scrollViewDidZoom:(U ...
  • 最近閑來無事,整理一下UICollectionView的相關方法以備使用 UICollectionViewFlowLayout和UICollectionViewLayout UICollectionViewFlowLayout是UICollectionViewLayout是一個子類,我們通常用的比較 ...
  • 作為從安卓的的入門選手,第一次看到還以為是個第三方呢,從github下來之後感覺不對啊,這麼多東西,後來一搜原來是個插件,而且不用從github上下載。 安裝的方法很簡單。 第一步:打開安卓studio的配置,找到Plugins,在右邊搜索ButterKnife ,你就會看到下麵這個界面。沒有錯,這 ...
  • 最近閑來無事,總結一下 UITableViewDataSource和 UITableViewDelegate方法 UITableViewDataSource @required - (NSInteger)tableView:(UITableView *)tableView numberOfRowsI ...
  • 作為安卓入門選手,在導入第三方的時候才發現居然有兩個build.gradle,我說咋不對啊,原來是導錯了(可能是因為我沒有看安卓培訓的視頻吧)。 那麼就說一下這兩個的作用(一個Project的,一個Module的): 簡單一點來說Project中的gradle是聲明的資源包括依賴項、第三方插件、ma ...
一周排行
    -Advertisement-
    Play Games
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...