1. 程式人生 > >Android記憶體洩漏分析

Android記憶體洩漏分析

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

Java的四種引用

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

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

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

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

下面是一個例子:

public classMain{

    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前獲取:[email protected] * GC後獲取:[email protected] * 佇列中的結果: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前獲取:[email protected] * GC後獲取:null * 佇列中的結果:[email protected] * */ 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前獲取:[email protected] * GC後獲取:null * 佇列中的結果:[email protected] * */ 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 那點事 ,總結的特別棒

導致記憶體洩漏的原因

物件在GC Root中可達,也就是他的引用不為空,所以GC無法回收它也就會導致記憶體洩漏

GC Root起點

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

GC可以續一秒

當一個物件在引用鏈中失去了引用,那麼他就真的要告別世界了嗎,其實並不是,虛擬機器會給他“緩刑”,每一個物件有一個finalize() 方法,虛擬機器是否給他緩刑取決於這個物件的這個方法是否被執行,如果這個物件的這個方法沒有被覆蓋或者這個方法被執行過一次,那麼就要“行刑”了。真的是“續一秒”

如果這個物件的finalize()方法應該被執行,那麼虛擬機器會將它放在F-Queue佇列中,稍後虛擬機器會自動建立一個Finalizer執行緒去執行這個佇列中的物件的這個方法。如果物件在finalize()中成功自救,舉個例子,把自己和一個存在的物件強引用,那麼就不會被回收,否則就真的被回收了。

但是虛擬機器並不會保證Finalizer執行緒執行結束再進行回收,因為如果在某一個物件的finalize()方法中執行了死迴圈或者超級耗時的操作,虛擬機器等待這個執行結束的話就會導致整個Gc崩潰了

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

併發GC和非併發GC

Android2.3之後支援了併發的GC。

  • 非併發GC: 虛擬機器在執行GC的時候進行Stop the world,也就是掛起其他所有的執行緒,通常會持續上百毫秒,一次Mark,然後直接清理

gc

  • 併發GC: 跟非併發的簡單gc來比較,一般非併發GC需要耗費上百ms的時間來進行,而併發gc僅僅需要10ms左右的時間,效率大幅度提升(資料來自:技術小黑屋大大),但是併發gc由於需要進行重複的處理改動的物件,所以需要更多的CPU資源

gc_bf

兩者的差別:

首先非併發GC簡單粗暴,直接掛起所有的執行緒,此時Java堆中肯定不會有任何的新增和修改,此時去遞迴GC樹,然後標記-清理。但是這樣會造成很大的開銷,大家都等著你豈不是很沒面子= =

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

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

  • 指標碰撞:一個指標,申請一塊記憶體就指標挪動相應的距離,不會產生記憶體碎片,這要求記憶體是很規整的
  • 空閒列表:每次申請一塊記憶體給需要的物件,然後有一個列表記錄了哪些位置被申請了,下次申請的時候就不申請這個位置,這樣適用於記憶體不是很規整的情況

建立物件是一個頻繁的操作,那麼我們如何保證原子性呢?兩種方案

  • CAS(Compare and Swap)策略配上失敗重試來保證原子性
  • 每個執行緒分配一個TLAB: 很簡單,每個執行緒自己有自己的一塊記憶體,那麼分配的時候自己鎖自己的分割槽就行了,提高了效率

我們用的是第二種 233

所以獲取Java堆鎖的時候,重點來了,我們逐個執行緒去鎖TLAB,而不是一次全鎖住,當然提高了併發GC的效率,所以更快。但是引來的問題就是併發的問題,所以下一步要重複去修改在一個個探索時候被改的物件。也就需要更多的CPU資源。

我們為什麼要關注GC

首先我們知道虛擬機器如何去GC才能瞭解到如何讓一個物件被正確的回收,這樣才不能記憶體洩漏

其次無論是併發GC還是非併發GC都會導致掛起其他的所有執行緒,那麼就會帶來程式卡頓。

ART在GC上做到了更加細粒度的控制,可以更加流暢的GC

常見的記憶體洩漏案例:Handler記憶體洩漏

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

public classMainActivityextendsAppCompatActivity{

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

參考文獻

相關推薦

LeakCanary Android 記憶體洩漏分析利器 原始碼編譯配置mk檔案

LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional LOCAL_SRC_FILES := \ $(call all-java-files-under, src) LOCAL_SRC_

Android記憶體洩漏分析及除錯

 尊重原創作者,轉載請註明出處: 首先了解一下dalvik的Garbage Collection: 如上圖所示,GC會選擇一些它瞭解還存活的物件作為記憶體遍歷的根節點(GC Roots),比方說thread stack中的變數,JNI中的全域性變數,zygote中

Mac Android 記憶體洩漏分析 實戰演練

虛的概念就不講了,自己去網上搜,一大堆。  這裡來一次實戰演練簡書上有一篇講解 記憶體洩漏分析 的文章,總結的很到位,由淺入深,比較全面。建議結合起來閱讀預備知識:1、Mac版 MAT 官網下載地址:  (也可以自行百度)http://www.eclipse.org/down

Android記憶體洩漏分析

Android記憶體洩漏是一個經常要遇到的問題,程式在記憶體洩漏的時候很容易導致OOM的發生。那麼如何查詢記憶體洩漏和避免記憶體洩漏就是需要知曉的一個問題,首先我們需要知道一些基礎知識。 Java的四種引用 強引用: 強引用是Java中最普通的引用,隨意建立一個物件然

Android記憶體洩漏問題分析及解決方案

大家新年好,由於工作繁忙原因,有好一段時間沒有更新博文了(當然Github是一直都有更新的),趁著年底有點放假時間,我覺得抽空更新下部落格,總結一下工作中最常見記憶體洩漏問題,也是自己之前踩過的坑,為了讓大家少走彎路,系統全面總結一下記憶體洩漏問題分析原因及尋找解決方案。 概念 首

Android記憶體洩漏框架LeakCanary原始碼分析

LeakCanary原始碼分析 LeakCanary是一個記憶體洩漏檢測的框架,預設只會檢測Activity的洩漏,如果需要檢測其他類,可以使用LeakCanary.install返回的RefWatcher,呼叫RefWatcher.watch(obj)就可以

有關Android Handler記憶體洩漏分析及解決辦法

1、Android的開發工具是java,這能幫助我們解決很底層的問題 包括:記憶體管理,平臺依賴。然而,有時候專案依然會報OOM錯誤,so垃圾收集器在哪? 2、我主要研究一種情況:記憶體中較大物件很長一段時間內不能被釋放。這方面並不完全算作記憶體溢位,物件會在某一時間點上被

Android記憶體管理機制和記憶體洩漏分析及優化

Android中的記憶體管理機制 分配機制 Android為每個程序分配記憶體的時候,採用了彈性的分配方式,也就是剛開始並不會一下分配很多記憶體給每個程序,而是給每一個程序分配一個“夠用”的量。這個量是根據每一個裝置實際的實體記憶體大小來決定的。隨著應用

MAT分析android記憶體洩漏

轉載請標明出處:https://www.cnblogs.com/tangZH/p/10955429.html   洩漏,洩漏,漏~ 記憶體洩漏怎麼破,什麼是記憶體洩漏?與記憶體溢位有什麼區別?   記憶體洩漏(Memory Leak):是指程式中己動態分配的堆記憶體由於某種原因程

[Android]Android記憶體洩漏你所要知道的一切(翻譯)

以下內容為原創,歡迎轉載,轉載請註明 來自天天部落格:http://www.cnblogs.com/tiantianbyconan/p/7235616.html Android記憶體洩漏你所要知道的一切 原文:https://blog.aritraroy.in/everything-

Android記憶體洩露分析

一,記憶體洩露 記憶體洩露:一個不在被使用的物件被另一個存活著的物件引用,在這種情況下垃圾回收器會跳過他,因為這種引用關係足以讓該物件駐留在記憶體中,記憶體洩露是在組織垃圾回收器為未來的記憶體分配提供空間,這些洩露的物件一直佔據著記憶體,導致我們的堆記憶體空間變得更小。也加劇了垃圾回

Android 記憶體洩漏問題

1. 內部類引用導致Activity的洩露     在Android中內部類的引用最常見的是handler,我們經常會這樣寫: private Handler handler = new Handler(){ @Override public void h

轉載:Android 記憶體洩露分析實戰演練

版權宣告:本文為博主原創文章,未經博主允許不得轉載。 https://mp.csdn.net/postedit/82736058 轉載自任玉剛微信推文,非常全面所以記錄下來 1. 記憶體洩露簡介 記憶體洩露,即Memory Leak,指程式中不再使用到的物件因某種原因從而無法被GC正常回

Android 記憶體洩漏之LeakCanary

導言: 記憶體管理是android開發效能中重要的一環,而leakCanary是Square開源框架,是一個Android記憶體洩露檢測庫,是個優秀的 記憶體洩露檢測工具,通過它大大降低oom的出現,提高app的質量 釋義: 記憶體洩漏:物件在有限生命週期內還持有引用,沒有被回

Android記憶體洩漏記憶體溢位

Android記憶體洩漏與記憶體溢位 記憶體洩漏 什麼是記憶體洩漏 記憶體洩漏的原因 記憶體洩漏檢測工具LeakCanary Java中的記憶體分配 Java中的四種引用型別 騰訊記憶體洩漏分析

Android記憶體洩漏的檢測工具——LeakCanary

    首先了解什麼是記憶體洩露 http://liuwangshu.cn/application/performance/ram-3-memory-leak.html   1Leakcancary的優勢 LeakCanary是一個視覺化的記憶體

Handler記憶體洩漏分析與解決方法

最近整理完Android中訊息機制的知識後,想到Handler記憶體洩漏相關的問題也可以順便整理一下,便有了這篇文章,也方便以後自己查閱 為什麼Handler會造成記憶體洩漏 下面是一段簡單的Handler使用 public class MainActivity extend

Android 效能篇 -- 帶你領略Android記憶體洩漏的前世今生

基礎瞭解 什麼是記憶體洩漏? 記憶體洩漏是當程式不再使用到的記憶體時,釋放記憶體失敗而產生了無用的記憶體消耗。記憶體洩漏並不是指物理上的記憶體消失,這裡的記憶體洩漏是指由程式分配的記憶體但是由於程式邏輯錯誤而導致程式失去了對該記憶體的控制,使得記憶體浪費。 Java

Android記憶體洩漏查詢和解決adb shell dumpsys meminfo packagement

1.通過adb shell dumpsys meminfo packageName來檢視記憶體使用狀況 在沒有開啟應用的情況下,該命令返回的資料是這樣的: 2.開啟這個應用的MainActivity,再通過命令檢視: 可以看到打印出來很多的資訊,而對於我們檢

android 記憶體洩漏小結

什麼是記憶體洩漏 記憶體洩漏是當程式不再使用到的記憶體時,釋放記憶體失敗而產生了無用的記憶體消耗。記憶體洩漏並不是指物理上的記憶體消失,這裡的記憶體洩漏是值由程式分配的記憶體但是由於程式邏輯錯誤而導致程式失去了對該記憶體的控制,使得記憶體浪費 怎樣會導致記憶體洩漏 資源物件沒關閉