Android 性能優化以內存泄漏檢測以及內存優化(上)

  在 Java 中,內存的分配是由程序完成的,而內存的釋放則是由 Garbage Collecation(GC) 完成的,Java/Android 程序員不用像 C/C++ 程序員同樣手動調用相關函數來管理內存的分配和釋放,雖然方便了不少,可是這也就形成了內存泄漏的可能性,因此記錄一下針對 Android 應用的內存泄漏的檢測,處理和優化的相關內容,上篇主要會分析 Java/Android 的內存分配以及 GC 的詳細分析,中篇會闡述 Android 內存泄漏的檢測和內存泄漏的常見產生情景,下篇會分析一下內存優化的內容。
  上篇:Android 性能優化以內存泄漏檢測以及內存優化(上)
  中篇:Android 性能優化以內存泄漏檢測以及內存優化(中)
  下篇:Android 性能優化以內存泄漏檢測以及內存優化(下)
  轉載請註明出處:blog.csdn.net/self_study/…
  對技術感興趣的同鞋加羣544645972一塊兒交流。javascript

Java/Android 內存分配和回收策略分析

  這裏須要提到的一點是在 Android 4.4 版本以前,使用的是和 Java 同樣的 Dalvik rumtime 機制,可是在 4.4 版本及之後,Android 引入了 ART 機制,ART 堆的分配與 GC 就和 Dalvik 的堆的分配與 GC 不同了,下面會介紹到(關於 Dalvik 和 ART 的對比:Android ART運行時無縫替換Dalvik虛擬機的過程分析)。html

Java/Android 內存分配策略

  Java/Android 程序運行時的內存分配有三種策略,分別是靜態的,棧式的和堆式的,對應的三種存儲策略使用的內存空間主要分別是靜態存儲區(方法區)、堆區和棧區:java

  • 靜態存儲區(方法區)
  • 內存在程序編譯的時候就已經分配好,這塊內存在程序整個運行期間都存在,它主要是用來存放靜態數據、全局 static 數據和常量;
  • 棧區
  • 在執行函數時,函數內部局部變量的存儲單元均可以在棧上建立,函數執行結束時這些存儲單元自動被釋放,棧內存分配運算內置於處理器的指令集中,效率很高,可是分配的內存容量有限;
  • 堆區
  • 亦稱爲動態內存分配,Java/Android 程序在適當的時候使用 new 關鍵字申請所須要大小的對象內存,而後經過 GC 決定在不須要這塊對象內存的時候回收它,可是因爲咱們的疏忽致使該對象在不須要繼續使用的以後,GC 仍然沒辦法回收該內存區域,這就表明發生了內存泄漏。
   堆區和棧區的區別:
  在函數中定義的一些基本類型的變量和對象的引用變量(也就是局部變量的引用)都是在函數的棧內存分配的,當在一段代碼塊中定義一個變量時,Java 就在棧中爲這個變量分配內存空間,當超過變量的做用域後,Java 會自動釋放掉爲該變量分配的內存空間,該內存空間能夠馬上被從新使用;堆內存用於存放全部由 new 建立的對象(內容包括該對象其中的全部成員變量)和數組,在堆中分配的內存是由 GC 來管理的,在堆中產生了一個對象或者數組後,還能夠在棧中生成一個引用指向這個堆中對象的內存區域,之後就能夠經過棧中這個引用變量來訪問堆中的這個引用指向的對象或者數組。下面這個圖片很好的說明了它兩的區別:
  
示例圖片

  堆是不連續的內存區域(由於系統是用鏈表來存儲空閒內存地址,因此隨着內存的分配和釋放,確定是不連續的),堆的大小受限於計算機系統中有效的虛擬內存(32bit 理論上是 4G),因此堆的空間比較大,也比較靈活;棧是一塊連續的內存區域,大小是操做系統預約好的,因爲存儲的都是基本數據類型和對象的引用,因此大小通常不會太大,在幾 M 左右。上面的這些差別致使頻繁的內存申請和釋放形成堆內存在大量的碎片,使得堆的運行效率下降,而對於棧來講,它是先進後出的隊列,不產生碎片,運行效率高。
  綜上所述:
  • 局部變量的基本數據類型和引用存儲於棧中,引用的對象實體存儲於堆中,由於它們屬於方法中的變量,生命週期隨方法而結束;
  • 成員變量所有存儲於堆中(包括基本數據類型,對象引用和引用指向的對象實體),由於它們屬於類,類對象終究是要被 new 出來使用的;
  • 咱們所說的內存泄露,只針對堆內存,他們存放的就是引用指向的對象實體。

Java 經常使用垃圾回收機制

  

  • 引用計數
  • 比較古老的回收算法,原理是此對象有一個引用,即增長一個計數,刪除一個引用則減小一個計數,垃圾回收時只用收集計數爲 0 的對象,此算法最致命的是沒法處理循環引用的問題;
  • 標記-清除收集器
  • 這種收集器首先遍歷對象圖並標記可到達的對象,而後掃描堆棧以尋找未標記對象並釋放它們的內存,這種收集器通常使用單線程工做並會暫停其餘線程操做,而且因爲它只是清除了那些未標記的對象,而並無對標記對象進行壓縮,致使會產生大量內存碎片,從而浪費內存;
  • 標記-壓縮收集器
  • 有時也叫標記-清除-壓縮收集器,與標記-清除收集器有相同的標記階段,可是在第二階段則把標記對象複製到堆棧的新域中以便壓縮堆棧,這種收集器也會暫停其餘操做;
  • 複製收集器(半空間)
  • 這種收集器將堆棧分爲兩個域,常稱爲半空間,每次僅使用一半的空間,JVM 生成的新對象則放在另外一半空間中,GC 運行時它把可到達對象複製到另外一半空間從而壓縮了堆棧,這種方法適用於短生存期的對象,持續複製長生存期的對象則致使效率下降,而且對於指定大小堆來講須要兩倍大小的內存,由於任什麼時候候都只使用其中的一半;
  • 增量收集器
  • 增量收集器把堆棧分爲多個域,每次僅從一個域收集垃圾,也可理解爲把堆棧分紅一小塊一小塊,每次僅對某一個塊進行垃圾收集,這就只會引發較小的應用程序中斷時間,使得用戶通常不能覺察到垃圾收集器運行;
  • 分代收集器
  • 複製收集器的缺點是每次收集時全部的標記對象都要被拷貝,從而致使一些生命週期很長的對象被來回拷貝屢次,消耗大量的時間,而分代收集器則可解決這個問題,分代收集器把堆棧分爲兩個或多個域用以存放不一樣壽命的對象,JVM 生成的新對象通常放在其中的某個域中,過一段時間,繼續存在的對象(非短命對象)將轉入更長壽命的域中,分代收集器對不一樣的域使用不一樣的算法以優化性能。

Java/Android 4.4 版本之下 Dalvik 虛擬機分析

Dalvik 堆簡介

  

這裏寫圖片描述

上圖爲 Dalvik 虛擬機的 Java 堆描述(出自: Dalvik虛擬機Java堆建立過程分析),如上圖所示,在 Dalvik 虛擬機中,Java 堆其實是由一個 Active 堆和一個 Zygote 堆組成的,其中 Zygote 堆用來管理 Zygote 進程在啓動過程當中預加載和建立的各類對象,而 Active 堆是在 Zygote 進程 fork 第一個子進程以前建立的,應用進程都是經過 Zygote 進程 fork 出來的(相關函數爲 ZygoteInit.main 函數: Android TransactionTooLargeException 解析,思考與監控方案),以後不管是 Zygote 進程仍是其子進程,都在 Active 堆上進行對象分配和釋放,這樣作的目的是使得 Zygote 進程和其子進程最大限度地共享 Zygote 堆所佔用的內存。上面講到應用程序進程是由 Zygote 進程 fork 出來的,也就是說應用程序進程使用了一種寫時拷貝技術(COW)來複制 Zygote 進程的地址空間,這意味着一開始的時候,應用程序進程和 Zygote 進程共享了同一個用來分配對象的堆,然而當 Zygote 進程或者應用程序進程對該堆進行寫操做時,內核纔會執行真正的拷貝操做,使得 Zygote 進程和應用程序進程分別擁有本身的一份拷貝。拷貝是一件費時費力的事情,所以爲了儘可能地避免拷貝,Dalvik 虛擬機將本身的堆劃分爲兩部分,事實上 Dalvik 虛擬機的堆最初是隻有一個的,也就是 Zygote 進程在啓動過程當中建立 Dalvik 虛擬機的時候只有一個堆,可是當 Zygote 進程在 fork 第一個應用程序進程以前會將已經使用了的那部分堆內存劃分爲一部分,尚未使用的堆內存劃分爲另一部分,前者就稱爲 Zygote 堆,後者就稱爲 Active 堆。之後不管是 Zygote 進程仍是應用程序進程,當它們須要分配對象的時候,都在 Active 堆上進行,這樣就可使得 Zygote 堆被應用進程和 Zygote 進程共享從而儘量少地被執行寫操做,因此就能夠減小執行寫時的拷貝操做。在 Zygote 堆裏面分配的對象其實主要就是 Zygote 進程在啓動過程當中預加載的類、資源和對象,這意味着這些預加載的類、資源和對象能夠在 Zygote 進程和應用程序進程中作到長期共享,這樣既能減小拷貝操做還能減小對內存的需求(出自: Dalvik虛擬機垃圾收集機制簡要介紹和學習計劃)。

Dalvik 分配內存過程分析

  

這裏寫圖片描述

上圖就是 Dalvik VM 爲新建立對象分配內存的過程(出自: Dalvik虛擬機爲新建立對象分配內存的過程分析),咱們來看看分配的具體步驟:
  1. Dalvik 虛擬機實現了一個 dvmAllocObject 函數,每當 Dalvik 虛擬機須要爲對象分配內存時,就會調用函數 dvmAllocObject,例如,當 Dalvik 虛擬機的解釋器遇到一個 new 指令時,它就會調用函數 dvmAllocObject;
  2. 函數 dvmAllocObject 調用函數 dvmMalloc 從 Java 堆中分配一塊指定大小的內存給新建立的對象使用,若是分配成功,那麼接下來就先使用宏 DVM_OBJECT_INIT 來初始化新建立對象的成員變量 clazz,使得新建立的對象能夠與某個特定的類關聯起來,接着再調用函數 dvmTrackAllocation 記錄當前的內存分配信息,以便通知 DDMS。函數 dvmMalloc 返回的只是一塊內存地址,這是沒有類型的,可是因爲每個 Java 對象都是從 Object 類繼承下來的,所以函數 dvmAllocObject 能夠將得到的沒有類型的內存塊強制轉換爲一個 Object 對象;
  3. dvmMalloc 函數接着調用到了另外一個函數 tryMalloc ,真正執行內存分配操做的就是這個 tryMalloc 函數,dvmMalloc 函數操做若是分配內存成功,則記錄當前線程成功分配的內存字節數和對象數等信息;不然的話,就記錄當前線程失敗分配的內存字節數和對象等信息,方便經過 DDMS 等工具對內存使用信息進行統計,同時會調用函數 throwOOME 拋出一個 OOM 異常;

void* dvmMalloc(size_t size, int flags)  
{  
    void *ptr;  

    dvmLockHeap();  

    /* Try as hard as possible to allocate some memory. */  
    ptr = tryMalloc(size);  
    if (ptr != NULL) {  
        /* We've got the memory. */  
        if (gDvm.allocProf.enabled) {  
            Thread* self = dvmThreadSelf();  
            gDvm.allocProf.allocCount++;  
            gDvm.allocProf.allocSize += size;  
            if (self != NULL) {  
                self->allocProf.allocCount++;  
                self->allocProf.allocSize += size;  
            }  
        }  
    } else {  
        /* The allocation failed. */  

        if (gDvm.allocProf.enabled) {  
            Thread* self = dvmThreadSelf();  
            gDvm.allocProf.failedAllocCount++;  
            gDvm.allocProf.failedAllocSize += size;  
            if (self != NULL) {  
                self->allocProf.failedAllocCount++;  
                self->allocProf.failedAllocSize += size;  
            }  
        }  
    }  

    dvmUnlockHeap();  

    if (ptr != NULL) {  
        /* * If caller hasn't asked us not to track it, add it to the * internal tracking list. */  
        if ((flags & ALLOC_DONT_TRACK) == 0) {  
            dvmAddTrackedAlloc((Object*)ptr, NULL);  
        }  
    } else {  
        /* * The allocation failed; throw an OutOfMemoryError. */  
        throwOOME();  
    }  

    return ptr;  
}複製代碼
  • 再來具體分析一下函數 tryMalloc,tryMalloc 會調用函數 dvmHeapSourceAlloc 在 Java 堆上分配指定大小的內存,若是分配成功,那麼就將分配獲得的地址直接返回給調用者了,函數 dvmHeapSourceAlloc 在不改變 Java 堆當前大小的前提下進行內存分配,這是屬於輕量級的內存分配動做;
  • 若是上一步內存分配失敗,這時候就須要執行一次 GC 了,不過若是 GC 線程已經在運行中,即 gDvm.gcHeap->gcRunning 的值等於 true,那麼就直接調用函數 dvmWaitForConcurrentGcToComplete 等到 GC 執行完成;不然的話,就須要調用函數 gcForMalloc 來執行一次 GC 了,參數 false 表示不要回收軟引用對象引用的對象;
  • static void *tryMalloc(size_t size)  
    {  
        void *ptr;  
        ......  
    
        ptr = dvmHeapSourceAlloc(size);  
        if (ptr != NULL) {  
            return ptr;  
        }  
    
        if (gDvm.gcHeap->gcRunning) {  
            ......  
            dvmWaitForConcurrentGcToComplete();  
        } else {  
            ......  
            gcForMalloc(false);  
        }  
    
        ptr = dvmHeapSourceAlloc(size);  
        if (ptr != NULL) {  
            return ptr;  
        }  
    
        ptr = dvmHeapSourceAllocAndGrow(size);  
        if (ptr != NULL) {  
            ......  
            return ptr;  
        }  
    
        gcForMalloc(true);  
        ptr = dvmHeapSourceAllocAndGrow(size);  
        if (ptr != NULL) {  
            return ptr;  
        }  
    
        ......  
    
        return NULL;  
    }複製代碼

  • GC 執行完畢後,再次調用函數 dvmHeapSourceAlloc 嘗試輕量級的內存分配操做,若是分配成功,那麼就將分配獲得的地址直接返回給調用者了;
  • 若是上一步內存分配失敗,這時候就得考慮先將 Java 堆的當前大小設置爲 Dalvik 虛擬機啓動時指定的 Java 堆最大值再進行內存分配了,這是經過調用函數 dvmHeapSourceAllocAndGrow 來實現的;
  • 若是調用函數 dvmHeapSourceAllocAndGrow 分配內存成功,則直接將分配獲得的地址直接返回給調用者了;
  • 若是上一步內存分配仍是失敗,這時候就得出狠招了,再次調用函數 gcForMalloc 來執行 GC,不過此次參數爲 true 表示要回收軟引用對象引用的對象;
  • 上一步 GC 執行完畢,再次調用函數 dvmHeapSourceAllocAndGrow 進行內存分配,這是最後一次努力了,若是還分配內存不成功那就是 OOM 了。
  • Dalvik GC 策略分析

      不一樣語言平臺進行標記回收內存的算法是不同的,Java 則是採用的 GC-Root 標記回收算法,在 Android 4,4 之下也是和 Java 同樣的機制(Android 4.4 和以後都是使用了 ART,和dalvik GC 有不一樣的地方),下面這張來自 Google IO 2011 大會的圖就很好的展現了Android 4.4 版本之下的回收策略:
      android

    這裏寫圖片描述

    圖中的每一個圓節點表明對象的內存資源,箭頭表明可達路徑,當一個圓節點和 GC Roots 存在可達的路徑時,表示當前它指向的內存資源正在被引用,虛擬機是沒法對其進行回收的(圖中的黃色節點);反過來,若是當前的圓節點和 GC Roots 不存在可達路徑,則意味着這塊對象的內存資源再也不被程序引用,系統虛擬機能夠在 GC 的時候將其內存回收掉。具體點來講,Java/Android 的內存垃圾回收機制是從程序的主要運行對象(如靜態對象/寄存器/棧上指向的內存對象等,對應上面的 GC Roots)開始檢查調用鏈,當遍歷一遍後獲得上述這些沒法回收的對象和他們所引用的對象鏈組成沒法回收的對象集合,而剩餘其餘的孤立對象(集)就做爲垃圾被 GC 回收。GC 爲了可以正確釋放對象,必須監控每個對象的運行狀態,包括對象的申請、引用、被引用、賦值等。監視對象狀態是爲了更加準確地、及時地釋放對象,而釋放對象的根本原則就是該對象再也不被引用。
      上面介紹了 GC 的回收機制,那麼接下來據此說一下什麼是內存泄漏,從抽象定義上講,Java/Android 平臺的內存泄漏是指沒有用的對象資源仍然和 GC Roots 保持可達路徑,致使系統沒法進行回收,具體一點講就是,經過 GC Roots 的調用鏈能夠遍歷到這個沒有被使用的對象,致使該資源沒法進行釋放。最多見的好比,Android 中的 Activity 中建立一個內部類 Handler 用來處理多線程的消息,而內部類會持有外部類的引用,因此該 Handler 的對象會持有 Activity 的引用,而若是這個 Handler 對象被子線程持有,子線程正在進行耗時的操做無法在短期內執行完成,那麼一系列的引用鏈致使 Activity 關閉以後一直沒法被釋放,重複地打開關閉這個 Activity 會形成這些 Activity 的對象一直在內存當中,最終達到必定程度以後會產生 OOM 異常。
      在 Java/Android 中,雖然咱們有幾個函數能夠訪問 GC,例如運行GC的函數 System.gc(),可是根據 Java 語言規範定義,該函數不保證 JVM 的垃圾收集器必定會立刻執行。由於不一樣的 JVM 實現者可能使用不一樣的算法管理 GC,一般 GC 的線程的優先級別較低。JVM 調用 GC 的策略也有不少種,有的是內存使用到達必定程度時 GC 纔開始工做,也有定時執行的,有的是平緩執行GC,也有的是中斷式執行GC,但一般來講咱們開發者不須要關心這些。

    Dalvik GC 日誌分析

      上面介紹到,雖然咱們有幾個函數能夠訪問 GC,可是該函數不會保證 GC 操做會立馬執行,那麼我怎麼去監聽系統的 GC 過程來實時分析當前的內存狀態呢?其實很簡單,Android 4.4 版本之下系統 Dalvik 每進行一次 GC 操做都會在 LogCat 中打印一條對應的日誌,咱們只須要去分析這條日誌就能夠了,日誌的基本格式以下:git

    D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>,  <Pause_time>複製代碼

    這段日誌分爲 4 個部分:程序員

    • 首先是第一部分 GC_Reason,就是觸發此次 GC 的緣由,通常狀況下有如下幾種緣由:
      • GC_CONCURRENT
      • 當咱們應用程序的堆內存快要滿的時候,系統會自動觸發 GC 操做來釋放內存;
      • GC_FOR_MALLOC
      • 當咱們的應用程序須要分配更多內存,但是現有內存已經不足的時候,系統會進行 GC 操做來釋放內存;
      • GC_HPROF_DUMP_HEAP
      • 當生成內存分析 HPROF 文件的時候,系統會進行 GC 操做,咱們下面會分析一下 HPROF 文件;
      • GC_EXPLICIT
      • 這種狀況就是咱們剛纔提到過的,主動通知系統去進行GC操做,好比調用 System.gc() 方法來通知系統,或者在 DDMS 中,經過工具按鈕也是能夠顯式地告訴系統進行 GC 操做的。

    • 第二部分 Amount_freed,表示系統經過此次 GC 操做釋放了多少的內存;
    • 第三部分 Heap_stats,表明當前內存的空閒比例以及使用狀況(活動對象所佔內存 / 當前程序總內存);
    • 第四部分 Pause_time,表明了此次 GC 操做致使應用程序暫停的時間,在 Android 2.3 版本以前 GC 操做是不能併發執行的,因此若是當系統正在 GC 的時候,應用程序只能阻塞等待 GC 結束,GC 的時間基本在幾百毫秒左右,因此用戶會感受到略微明顯的卡頓,體驗很差,在 Android 2.3 以及以後到 4.4 版本以前,Dalvik GC 的操做改爲了併發執行,也就是說 GC 的操做不會影響到主應用程序的正常運行,可是 GC 操做的開始和結束仍然會短暫的阻塞一段時間,不過期間上面就已經短到讓用戶沒法察覺到了。

    • Android 4.4 及以上 ART 分析

      ART 堆簡介

        

      這裏寫圖片描述

      上圖爲 ART 堆的描述(圖片出自: ART運行時垃圾收集機制簡要介紹和學習計劃),ART 也涉及到相似於 Dalvik 虛擬機的 Zygote 堆、Active 堆、Card Table、Heap Bitmap 和 Mark Stack 等概念。從圖中能夠看到,ART 運行時堆劃分爲四個空間,分別是 Image Space、Zygote Space、Allocation Space 和 Large Object Space,其中 Image Space、Zygote Space 和 Allocation Space 是在地址上連續的空間,稱爲 Continuous Space,而 Large Object Space 是一些離散地址的集合,用來分配一些大對象,稱爲 Discontinuous Space。
        在 Image Space 和 Zygote Space 之間,隔着一段用來映射 system@framework@boot.art@classes.oat 文件的內存,system@framework@boot.art@classes.oat 是一個 OAT 文件,它是由在系統啓動類路徑中的全部 .dex 文件翻譯獲得的,而 Image Space 空間就包含了那些須要預加載的 系統類對象,這意味着須要預加載的類對象是在生成 system@framework@boot.art@classes.oat 這個 OAT 文件的時候建立而且保存在文件 system@framework@boot.art@classes.dex 中,之後只要系統啓動類路徑中的 .dex 文件不發生變化(即不發生更新升級),那麼之後每次系統啓動只須要將文件 system@framework@boot.art@classes.dex 直接映射到內存便可,省去了建立各個類對象的時間。以前使用 Dalvik 虛擬機做爲應用程序運行時的時候,每次系統啓動都須要爲那些預加載的系統類建立類對象,而雖然 ART 運行時第一次啓動會和 Dalvik 同樣比較慢,可是之後啓動實際上會快很多。因爲 system@framework@boot.art@classes.dex 文件保存的是一些預先建立的對象,而且這些對象之間可能會互相引用,所以咱們必須保證 system@framework@boot.art@classes.dex 文件每次加載到內存的地址都是固定的,這個固定的地址保存在 system@framework@boot.art@classes.dex 文件頭部的一個 Image Header 中,此外 system@framework@boot.art@classes.dex 文件也依賴於 system@framework@boot.art@classes.oat 文件,因此也會將後者固定加載到 Image Space 的末尾。
        Zygote Space 和 Allocation Space 與上面講到的 Dalvik 虛擬機中的 Zygote 堆和 Active 堆的做用是同樣的,Zygote Space 在 Zygote 進程和應用程序進程之間共享的,而 Allocation Space 則是每一個進程獨佔的。一樣的 Zygote 進程一開始只有一個 Image Space 和一個 Zygote Space,在 Zygote 進程 fork 出第一個子進程以前,就會把 Zygote Space 一分爲二,原來的已經被使用的那部分堆還叫 Zygote Space,而未使用的那部分堆就叫 Allocation Space,之後的對象都在新分出來的 Allocation Space 上分配,經過上述這種方式,就可使得 Image Space 和 Zygote Space 在 Zygote 進程和應用程序進程之間進行共享,而 Allocation Space 就每一個進程都獨立地擁有一份,和 Dalvik 一樣既能減小拷貝操做還能減小對內存的需求。有一點須要注意的是雖然 Image Space 和 Zygote Space 都是在 Zygote 進程和應用程序進程之間進行共享的,可是前者的對象只建立一次然後者的對象須要在系統每次啓動時根據運行狀況都從新建立一遍(出自: ART運行時垃圾收集機制簡要介紹和學習計劃)。
        ART 運行時提供了兩種 Large Object Space 實現,其中一種實現和 Continuous Space 的實現相似,預先分配好一塊大的內存空間,而後再在上面爲對象分配內存塊,不過這種方式實現的 Large Object Space 不像 Continuous Space 經過 C 庫的內塊管理接口來分配和釋放內存,而是本身維護一個 Free List,每次爲對象分配內存時,都是從這個 Free List 找到合適的空閒的內存塊來分配,釋放內存的時候,也是將要釋放的內存添加到該 Free List 去;另一種 Large Object Space 實現是每次爲對象分配內存時,都單獨爲其映射一新的內存,也就是說,爲每個對象分配的內存塊都是相互獨立的,這種實現方式相比上面介紹的 Free List 實現方式更簡單一些。在 Android 4.4 中,ART 運行時使用的是後一種實現方式,爲每一對象映射一塊獨立的內存塊的 Large Object Space 實現稱爲 LargeObjectMapSpace,它與 Free List 方式的實現都是繼承於類 LargeObjectSpace,LargeObjectSpace 又分別繼承了 DiscontinuousSpace 和 AllocSpace,所以咱們就能夠知道,LargeObjectMapSpace 描述的是一個在地址空間上不連續的 Large Object Space。

      ART 分配內存過程分析

        

      這裏寫圖片描述

      上圖就是 ART 爲新建立對象分配內存的過程(出自: ART運行時爲新建立對象分配內存的過程分析),能夠看到 ART 爲新建立對象分配內存的過程和 Dalvik VM 幾乎是同樣的,區別僅僅在於垃圾收集的方式和策略不同。
        ART 運行時爲從 DEX 字節碼翻譯獲得的 Native 代碼提供的一個函數調用表中,有一個 pAllocObject 接口是用來分配對象的,當 ART 運行時以 Quick 模式運行在 ARM 體系結構時,上述提到的 pAllocObject 接口由函數 art_quick_alloc_object 來實現,art_quick_alloc_object 是一段彙編代碼,最終通過一系列的調用以後最終會調用 ART 運行時內部的 Heap 對象的成員函數 AllocObject 在堆上分配對象(具體的過程: ART運行時爲新建立對象分配內存的過程分析),其中要分配的大小保存在當前 Class 對象的成員變量 object size 中。 Heap 類的成員函數 AllocObject 首先是要肯定要在哪一個 Space 上分配內存,能夠分配內存的 Space 有三個,分別 Zygote Space、Allocation Space 和 Large Object Space,不過 Zygote Space 在尚未劃分出 Allocation Space 以前就在 Zygote Space 上分配,而當 Zygote Space 劃分出 Allocation Space 以後,就只能在 Allocation Space 上分配,同時 Heap 類的成員變量 alloc space 在 Zygote Space 尚未劃分出 Allocation Space 以前指向 Zygote Space,而劃分以後就指向 Allocation Space,Large Object Space 則始終由 Heap 類的成員變量 large_object space 指向。只要知足如下三個條件就在 Large Object Space 上分配,不然就在 Zygote Space 或者 Allocation Space 上分配:
      1. 請求分配的內存大於等於 Heap 類的成員變量 large_objectthreshold 指定的值,這個值等於 3 * kPageSize,即 3 個頁面的大小;
      2. 已經從 Zygote Space 劃分出 Allocation Space,即 Heap 類的成員變量 have_zygotespace 的值等於 true;
      3. 被分配的對象是一個原子類型數組,即 byte 數組、int 數組和 boolean 數組等。
      肯定好要在哪一個 Space 上分配內存以後,就能夠調用 Heap 類的成員函數 Allocate 進行分配了,若是分配成功,Heap 類的成員函數 Allocate 就返回新分配的對象而且將該對象保存在變量 obj 中,接下來再會作三件事情:
      1. 調用 Object 類的成員函數 SetClass 設置新分配對象 obj 的類型;
      2. 調用 Heap 類的成員函數 RecordAllocation 記錄當前的內存分配情況;
      3. 檢查當前已經分配出去的內存是否已經達到由 Heap 類的成員變量 concurrent_startbytes 設定的閥值,若是已經達到,那麼就調用 Heap 類的成員函數 RequestConcurrentGC 通知 GC 執行一次並行 GC。
      另外一方面若是 Heap 類的成員函數 Allocate 分配內存失敗,則 Heap 類的成員函數 AllocObject 拋出一個 OOM 異常。Heap 類的 AllocObject 函數又會調用到成員函數 Allocate:

      mirror::Object* Heap::AllocObject(Thread* self, mirror::Class* c, size_t byte_count) {  
        ......  
      
        mirror::Object* obj = NULL;  
        size_t bytes_allocated = 0;  
        ......  
      
        bool large_object_allocation =  
            byte_count >= large_object_threshold_ && have_zygote_space_ && c->IsPrimitiveArray();  
        if (UNLIKELY(large_object_allocation)) {  
          obj = Allocate(self, large_object_space_, byte_count, &bytes_allocated);  
          ......  
        } else {  
          obj = Allocate(self, alloc_space_, byte_count, &bytes_allocated);  
          ......  
        }  
      
        if (LIKELY(obj != NULL)) {  
          obj->SetClass(c);  
          ......  
      
          RecordAllocation(bytes_allocated, obj);  
          ......  
      
          if (UNLIKELY(static_cast<size_t>(num_bytes_allocated_) >= concurrent_start_bytes_)) {  
            ......  
            SirtRef<mirror::Object> ref(self, obj);  
            RequestConcurrentGC(self);  
          }  
          ......  
      
          return obj;  
        } else {  
          ......  
          self->ThrowOutOfMemoryError(oss.str().c_str());  
          return NULL;  
        }  
      }複製代碼

      函數 Allocate 首先調用成員函數 TryToAllocate 嘗試在不執行 GC 的狀況下進行內存分配,若是分配失敗再調用成員函數 AllocateInternalWithGc 進行帶 GC 的內存分配,Allocate 是一個模板函數,不一樣類型的 Space 會致使調用不一樣重載的成員函數 TryToAllocate 進行不帶 GC 的內存分配。雖然能夠用來分配內存的 Space 有 Zygote Space、Allocation Space 和 Large Object Space 三個,可是前二者的類型是相同的,所以實際上只有兩個不一樣重載版本的成員函數 TryToAllocate,它們的實現以下所示:github

      inline mirror::Object* Heap::TryToAllocate(Thread* self, space::AllocSpace* space, size_t alloc_size,  
                                                 bool grow, size_t* bytes_allocated) {  
        if (UNLIKELY(IsOutOfMemoryOnAllocation(alloc_size, grow))) {  
          return NULL;  
        }  
        return space->Alloc(self, alloc_size, bytes_allocated);  
      }  
      
      // DlMallocSpace-specific version. 
      inline mirror::Object* Heap::TryToAllocate(Thread* self, space::DlMallocSpace* space, size_t alloc_size,  
                                                 bool grow, size_t* bytes_allocated) {  
        if (UNLIKELY(IsOutOfMemoryOnAllocation(alloc_size, grow))) {  
          return NULL;  
        }  
        if (LIKELY(!running_on_valgrind_)) {  
          return space->AllocNonvirtual(self, alloc_size, bytes_allocated);  
        } else {  
          return space->Alloc(self, alloc_size, bytes_allocated);  
        }  
      }複製代碼

      Heap 類兩個重載版本的成員函數 TryToAllocate 的實現邏輯都幾乎是相同的,首先是調用另一個成員函數 IsOutOfMemoryOnAllocation 判斷分配請求的內存後是否會超過堆的大小限制,若是超過則分配失敗;不然的話再在指定的 Space 進行內存分配。函數IsOutOfMemoryOnAllocation的實現以下所示:算法

      inline bool Heap::IsOutOfMemoryOnAllocation(size_t alloc_size, bool grow) {  
        size_t new_footprint = num_bytes_allocated_ + alloc_size;  
        if (UNLIKELY(new_footprint > max_allowed_footprint_)) {  
          if (UNLIKELY(new_footprint > growth_limit_)) {  
            return true;  
          }  
          if (!concurrent_gc_) {  
            if (!grow) {  
              return true;  
            } else {  
              max_allowed_footprint_ = new_footprint;  
            }  
          }  
        }  
        return false;  
      }複製代碼

      成員變量 num_bytesallocated 描述的是目前已經分配出去的內存字節數,成員變量 max_allowedfootprint 描述的是目前堆可分配的最大內存字節數,成員變量 growthlimit 描述的是目前堆容許增加到的最大內存字節數,這裏須要注意的一點是 max_allowedfootprint 是 Heap 類施加的一個限制,不會對各個 Space 實際可分配的最大內存字節數產生影響,而且各個 Space 在建立的時候,已經把本身可分配的最大內存數設置爲容許使用的最大內存字節數。若是目前堆已經分配出去的內存字節數再加上請求分配的內存字節數 new_footprint 小於等於目前堆可分配的最大內存字節數 max_allowedfootprint,那麼分配出請求的內存字節數以後不會形成 OOM,所以 Heap 類的成員函數 IsOutOfMemoryOnAllocation 就返回false;另外一方面,若是目前堆已經分配出去的內存字節數再加上請求分配的內存字節數 new_footprint 大於目前堆可分配的最大內存字節數 max_allowedfootprint,而且也大於目前堆容許增加到的最大內存字節數 growthlimit,那麼分配出請求的內存字節數以後形成 OOM,所以 Heap 類的成員函數 IsOutOfMemoryOnAllocation 這時候就返回 true。
        剩下另一種狀況,目前堆已經分配出去的內存字節數再加上請求分配的內存字節數 new_footprint 大於目前堆可分配的最大內存字節數 max_allowedfootprint,可是小於等於目前堆容許增加到的最大內存字節數 growthlimit,這時候就要看狀況會不會出現 OOM 了:若是 ART 運行時運行在非並行 GC 的模式中,即 Heap 類的成員變量 concurrentgc 等於 false,那麼取決於允不容許增加堆的大小,即參數 grow 的值,若是不容許,那麼 Heap 類的成員函數 IsOutOfMemoryOnAllocation 就返回 true,表示當前請求的分配會形成 OOM,若是容許,那麼 Heap 類的成員函數 IsOutOfMemoryOnAllocation 就會修改目前堆可分配的最大內存字節數 max_allowedfootprint 而且返回 false,表示容許當前請求的分配,這意味着在非並行 GC 運行模式中,若是分配內存過程當中遇到內存不足而且當前可分配內存還未達到增加上限時,要等到執行完成一次非並行 GC 後才能成功分配到內存,由於每次執行完成 GC 以後都會按照預先設置的堆目標利用率來增加堆的大小;另外一方面,若是 ART 運行時運行在並行 GC 的模式中,那麼只要當前堆已經分配出去的內存字節數再加上請求分配的內存字節數 new_footprint 不超過目前堆容許增加到的最大內存字節數 growthlimit,那麼就無論允不容許增加堆的大小都認爲不會發生 OOM,所以 Heap 類的成員函數 IsOutOfMemoryOnAllocation 就返回 false,這意味着在並行 GC 運行模式中,在分配內存過程當中遇到內存不足,而且當前可分配內存還未達到增加上限時,無需等到執行並行 GC 後就有可能成功分配到內存,由於實際執行內存分配的 Space 可分配的最大內存字節數是足夠的。數組

      ART GC 策略以及過程分析

        在 Android 4.4 版本以及以後就使用了 ART 運行時,在安裝的時候就將應用翻譯成機器碼執行,效率比起之前的 Dalvik 虛擬機更高,可是缺點就是安裝以後的應用體積變大和安裝的時間會變長,不過相對於優勢來講,這點缺點不算什麼。ART 運行時與 Dalvik 虛擬機同樣,都使用了 Mark-Sweep 算法進行垃圾回收,所以它們的垃圾回收流程在整體上是一致的,可是 ART 運行時對堆的劃分更加細緻,於是在此基礎上實現了更多樣的回收策略。不一樣的策略有不一樣的回收力度,力度越大的回收策略每次回收的內存就越多,而且它們都有各自的使用情景,這樣就可使得每次執行 GC 時,能夠最大限度地減小應用程序停頓:
      性能優化

      這裏寫圖片描述

      上圖描述了 ART 運行時的垃圾收集收集過程(圖片出自: ART運行時垃圾收集(GC)過程分析),最上面三個箭頭描述觸發 GC 的三種狀況,左邊的流程圖描述非並行 GC 的執行過程,右邊的流程圖描述並行 GC 的執行流程,過程以下所示:
      • 非並行 GC :
        1. 調用子類實現的成員函數 InitializePhase 執行 GC 初始化階段;
        2. 掛起全部的 ART 運行時線程;
        3. 調用子類實現的成員函數 MarkingPhase 執行 GC 標記階段;
        4. 調用子類實現的成員函數 ReclaimPhase 執行 GC 回收階段;
        5. 恢復第 2 步掛起的 ART 運行時線程;
        6. 調用子類實現的成員函數 FinishPhase 執行 GC 結束階段。
      • 並行 GC :
        1. 調用子類實現的成員函數 InitializePhase 執行 GC 初始化階段;
        2. 獲取用於訪問 Java 堆的鎖;
        3. 調用子類實現的成員函數 MarkingPhase 執行 GC 並行標記階段;
        4. 釋放用於訪問 Java 堆的鎖;
        5. 掛起全部的 ART 運行時線程;
        6. 調用子類實現的成員函數 HandleDirtyObjectsPhase 處理在 GC 並行標記階段被修改的對象;
        7. 恢復第 5 步掛起的 ART 運行時線程;
        8. 重複第 5 到第 7 步,直到全部在 GC 並行階段被修改的對象都處理完成;
        9. 獲取用於訪問 Java 堆的鎖;
        10. 調用子類實現的成員函數 ReclaimPhase 執行 GC 回收階段;
        11. 釋放用於訪問 Java 堆的鎖;
        12. 調用子類實現的成員函數 FinishPhase 執行 GC 結束階段。
      它們的區別在於:
      1. 非並行 GC 的標記階段和回收階段是在掛住全部的 ART 運行時線程的前提下進行的,所以只須要執行一次標記便可;
      2. 並行 GC 的標記階段只鎖住了Java 堆,所以它不能阻止那些不是正在分配對象的 ART 運行時線程同時運行,而這些同時進運行的 ART 運行時線程可能會引用了一些在以前的標記階段沒有被標記的對象,若是不對這些對象進行從新標記的話,那麼就會致使它們被 GC 回收形成錯誤,所以與非並行 GC 相比,並行 GC 多了一個處理髒對象的階段,所謂的髒對象就是咱們前面說的在 GC 標記階段同時運行的 ART 運行時線程訪問或者修改過的對象;
      3. 並行 GC 並非自始至終都是並行的,例如處理髒對象的階段就是須要掛起除 GC 線程之外的其它 ART 運行時線程,這樣才能夠保證標記階段能夠結束。

        上面 ART 堆內存分配的時候,咱們提到了有兩種可能會觸發 GC 的狀況,第一種狀況是沒有足夠內存分配給請求時,會調用 Heap 類的成員函數 CollectGarbageInternal 觸發一個緣由爲 kGcCauseForAlloc 的 GC;第二種狀況下分配出請求的內存以後,堆剩下的內存超過必定的閥值,就會調用 Heap 類的成員函數 RequestConcurrentGC 請求執行一個並行 GC;此外,還有第三種狀況會觸發GC,以下所示:

      void Heap::CollectGarbage(bool clear_soft_references) {  
        // Even if we waited for a GC we still need to do another GC since weaks allocated during the 
        // last GC will not have necessarily been cleared. 
        Thread* self = Thread::Current();  
        WaitForConcurrentGcToComplete(self);  
        CollectGarbageInternal(collector::kGcTypeFull, kGcCauseExplicit, clear_soft_references);  
      }複製代碼

      當咱們調用 Java 層的 java.lang.System 的靜態成員函數 gc 時,若是 ART 運行時支持顯式 GC,那麼它就會經過 JNI 調用 Heap 類的成員函數 CollectGarbageInternal 來觸發一個緣由爲 kGcCauseExplicit 的 GC,ART 運行時默認是支持顯式 GC 的,可是能夠經過啓動選項 -XX:+DisableExplicitGC 來關閉。因此 ART 運行時在三種狀況下會觸發 GC,這三種狀況經過三個枚舉 kGcCauseForAlloc、kGcCauseBackground 和 kGcCauseExplicitk 來描述:

      // What caused the GC? 
      enum GcCause {  
        // GC triggered by a failed allocation. Thread doing allocation is blocked waiting for GC before 
        // retrying allocation. 
        kGcCauseForAlloc,  
        // A background GC trying to ensure there is free memory ahead of allocations. 
        kGcCauseBackground,  
        // An explicit System.gc() call. 
        kGcCauseExplicit,  
      };複製代碼

      ART 運行時的全部 GC 都是以 Heap 類的成員函數 CollectGarbageInternal 爲入口:

      collector::GcType Heap::CollectGarbageInternal(collector::GcType gc_type, GcCause gc_cause,  
                                                     bool clear_soft_references) {  
        Thread* self = Thread::Current();  
        ......  
      
        // Ensure there is only one GC at a time. 
        bool start_collect = false;  
        while (!start_collect) {  
          {  
            MutexLock mu(self, *gc_complete_lock_);  
            if (!is_gc_running_) {  
              is_gc_running_ = true;  
              start_collect = true;  
            }  
          }  
          if (!start_collect) {  
            // TODO: timinglog this. 
            WaitForConcurrentGcToComplete(self);  
            ......  
          }  
        }  
      
        ......  
      
        if (gc_type == collector::kGcTypeSticky &&  
            alloc_space_->Size() < min_alloc_space_size_for_sticky_gc_) {  
          gc_type = collector::kGcTypePartial;  
        }  
      
        ......  
      
        collector::MarkSweep* collector = NULL;  
        for (const auto& cur_collector : mark_sweep_collectors_) {  
          if (cur_collector->IsConcurrent() == concurrent_gc_ && cur_collector->GetGcType() == gc_type) {  
            collector = cur_collector;  
            break;  
          }  
        }  
        ......  
      
        collector->clear_soft_references_ = clear_soft_references;  
        collector->Run();  
        ......  
      
        {  
            MutexLock mu(self, *gc_complete_lock_);  
            is_gc_running_ = false;  
            last_gc_type_ = gc_type;  
            // Wake anyone who may have been waiting for the GC to complete. 
            gc_complete_cond_->Broadcast(self);  
        }  
      
        ......  
      
        return gc_type;  
      }複製代碼

      參數 gc_type 和 gc_cause 分別用來描述要執行的 GC 的類型和緣由,而參數 clear_soft_references 用來描述是否要回收被軟引用指向的對象,Heap 類的成員函數 CollectGarbageInternal 的執行邏輯:

      1. 經過一個 while 循環不斷地檢查 Heap 類的成員變量 is_gcrunning,直到它的值等於 false 爲止,這表示當前沒有其它線程正在執行 GC,當它的值等於 true 時就表示其它線程正在執行 GC,這時候就要調用 Heap 類的成員函數 WaitForConcurrentGcToComplete 等待其執行完成,注意在當前 GC 執行以前,Heap 類的成員變量 is_gcrunning 會被設置爲true;
      2. 若是當前請求執行的 GC 類型爲 kGcTypeSticky,可是當前 Allocation Space 的大小小於 Heap 類的成員變量 min_alloc_space_size_for_stickygc 指定的閥值,那麼就改成執行類型爲 kGcTypePartial;
      3. 從 Heap 類的成員變量 mark_sweepcollectors 指向的一個垃圾收集器列表找到一個合適的垃圾收集器來執行 GC,ART 運行時在內部建立了六個垃圾收集器,這六個垃圾收集器分爲兩組,一組支持並行 GC,另外一組不支持;每一組都是由三個類型分別爲 kGcTypeSticky、kGcTypePartial 和 kGcTypeFull 的垃垃圾收集器組成,這裏說的合適的垃圾收集器是指並行性與 Heap 類的成員變量 concurrentgc 一致,而且類型也與參數 gc_type 一致的垃圾收集器;
      4. 找到合適的垃圾收集器以後,就將參數 clear_soft_references 的值保存在它的成員變量 clear_softreferences 中,以即可以告訴它要不要回收被軟引用指向的對象,而後再調用它的成員函數 Run 來執行 GC;
      5. GC 執行完畢,將 Heap 類的成員變量 is_gcrunning 設置爲false,以表示當前 GC 已經執行完畢,下一次請求的 GC 能夠執行了,此外也會將 Heap 類的成員變量 last_gctype 設置爲當前執行的 GC 的類型,這樣下一次執行 GC 時,就能夠執行另一個不一樣類型的 GC,例如若是上一次執行的 GC 的類型爲 kGcTypeSticky,那麼接下來的兩次 GC 的類型就能夠設置爲 kGcTypePartial 和 kGcTypeFull,這樣可使得每次都能執行有效的 GC;
      6. 經過 Heap 類的成員變量 gc_completecond 喚醒那些正在等待 GC 執行完成的線程。

      ART GC 與 Dalvik GC 對比

        比起 Dalvik 的回收策略,ART 的 CMS(concurrent mark sweep,同步標記回收)有如下幾個優勢:

      • 阻塞的次數相比於 Dalvik 來講,從兩次減小到了一次,Dalvik 第一次的阻塞大部分工做是在標記 root,而在 ART CMS 中則是被每一個執行線程同步標記它們本身的 root 完成的,因此 ART 可以立馬繼續運行;
      • 和 Dalvik 相似,ART GC 一樣在回收執行以前有一次暫停,可是關鍵的不一樣是 Dalvik 的一些執行階段在 ART 中是並行同步執行的,這些階段包括標記過程、系統 weak Reference 清理過程(好比 jni weak globals 等)、從新標記非 GC Root 節點,Card 區域的提早清理。在 ART 中仍然須要阻塞的過程是掃描 Card 區域的髒數據和從新標記 GC Root,這兩個操做可以下降阻塞的時間;
      • 還有一個 ART GC 比 Dalvik 有提高的地方是 sticky CMS 提高了 GC 的吞吐量,不像正常的分代 GC 機制,sticky CMS 是不移動堆內存的,它不會給新對象分配一個特定的區域(年輕代),新分配的對象被保存在一個分配棧裏面,這個棧就是一個簡單的 Object 數組,這就避免了所須要移動對象的操做,也就得到了低阻塞性,可是缺點就是會增長堆的對象複雜性;

        若是應用程序在前臺運行時,這時候 GC 被稱爲 Foreground GC,同時 ART 還有一個 Background GC,顧名思義就是在後臺運行的 GC,應用程序在前臺運行時響應性是最重要的,所以也要求執行的 GC 是高效的,相反應用程序在後臺運行時,響應性不是最重要的,這時候就適合用來解決堆的內存碎片問題,所以上面提到的全部 Mark-Sweep GC 適合做爲 Foreground GC,而 Compacting GC(壓縮 GC) 適合做爲 Background GC。當從 Foreground GC 切換到 Background GC,或者從 Background GC 切換到 Foreground GC,ActivityManager 會通知發生一次 Compacting GC 的行爲,這是因爲 Foreground GC 和 Background GC 的底層堆空間結構是同樣的,所以發生 Foreground GC 和 Background GC 切換時,須要將當前存活的對象從一個 Space 轉移到另一個 Space 上去,這個恰好就是 Semi-Space compaction 和 Homogeneous space compaction 適合乾的事情。Background GC 壓縮內存就可以使得內存碎片變少,從而達到縮減內存的目的,可是壓縮內存的時候會暫時阻塞應用進程。 Semi-Space compaction 和 Homogeneous space compaction 有一個共同特色是都具備一個 From Space 和一個 To Space,在 GC 執行期間,在 From Space 分配的還存活的對象會被依次拷貝到 To Space 中,這樣就能夠達到消除內存碎片的目的。與 Semi-Space compaction 相比,Homogeneous space compaction 還多了一個 Promote Space,當一個對象是在上一次 GC 以前分配的,而且在當前 GC 中仍然是存活的,那麼它就會被拷貝到 Promote Space 而不是 To Space 中,這至關因而簡單地將對象劃分爲新生代和老生代的,即在上一次 GC 以前分配的對象屬於老生代的,而在上一次 GC 以後分配的對象屬於新生代的,通常來講老生代對象的存活性要比新生代的久,所以將它們拷貝到 Promote Space 中去,能夠避免每次執行 Semi-Space compaction 或者 Homogeneous space compaction 時都須要對它們進行無用的處理,咱們來看看這兩種 Background GC 的執行過程圖:

      Semi-Space compaction

      Semi-Space compaction

      這裏寫圖片描述

      Homogeneous space compaction

      以上圖片來自: ART運行時Semi-Space(SS)和Generational Semi-Space(GSS)GC執行過程分析,Bump Pointer Space 1 和 Bump Pointer Space 2 就是咱們前面說的 From Space 和 To Space。Semi-Space compaction 通常發生在低內存的設備上,而 Homogenous space compaction 是非低內存設備上的默認壓縮模式。

      GC Roots 解析

        GC Roots 特指的是垃圾收集器(Garbage Collector)的對象,GC 會收集那些不是 GC Roots 且沒有被 GC Roots 引用的對象,一個對象能夠屬於多個 Root,GC Roots 有幾下種:

      • Class
      • 由系統類加載器(system class loader)加載的對象,這些類是不可以被回收的,他們能夠以靜態字段的方式持有其它對象。咱們須要注意的一點就是,經過用戶自定義的類加載器加載的類,除非相應的 java.lang.Class 實例以其它的某種(或多種)方式成爲 Roots,不然它們並非 Roots;
      • Thread
      • 活着的線程;
      • Stack Local
      • Java 方法的 local 變量或參數;
      • JNI Local
      • JNI 方法的 local 變量或參數;
      • JNI Global
      • 全局 JNI 引用;
      • Monitor Used
      • 用於同步的監控對象;
      • Held by JVM
      • 用於 JVM 特殊目的由 GC 保留的對象,但實際上這個與 JVM 的實現是有關的,可能已知的一些類型是系統類加載器、一些 JVM 熟悉的重要異常類、一些用於處理異常的預分配對象以及一些自定義的類加載器等,然而 JVM 並無爲這些對象提供其它的信息,所以就只有留給分析分員去肯定哪些是屬於 "JVM 持有" 的了。
      來源: www.yourkit.com/docs/java/h…

      ART 日誌分析

        ART 的 log 不一樣於 Dalvik 的 log 機制,不是明確調用的狀況下不會打印的 GCs 的 log 信息,GC只會在被斷定爲很慢時輸出信息,更準確地說就是 GC 暫停的時間超過 5ms 或者 GC 執行的總時間超過 100ms。若是 app 不是處於一種停頓可察覺的狀態,那麼 GC 就不會被斷定爲執行緩慢,可是此時顯式 GC 信息會被 log 出來,參考自:Investigating Your RAM Usage

      I/art: <GC_Reason> <GC_Name> <Objects_freed>(<Size_freed>) AllocSpace Objects, <Large_objects_freed>(<Large_object_size_freed>) <Heap_stats> LOS objects, <Pause_time(s)>複製代碼

      例如:

      I/art : Explicit concurrent mark sweep GC freed 104710(7MB) AllocSpace objects, 21(416KB) LOS objects, 33% free, 25MB/38MB, paused 1.230ms total 67.216ms複製代碼

      • GC Reason :什麼觸發了GC,以及屬於哪一種類型的垃圾回收,可能出現的值包括
        • Concurrent
        • 併發 GC,不會掛起 app 線程,這種 GC 在後臺線程中運行,不會阻止內存分配;
        • Alloc
        • GC 被初始化,app 在 heap 已滿的時候請求分配內存,此時 GC 會在當前線程(請求分配內存的線程)執行;
        • Explicit
        • GC 被 app 顯式請求,例如經過調用 System.gc() 或者 runtime.gc(),和 Dalvik 同樣,ART 建議相信 GC,儘量地避免顯式調用 GC,不建議顯式調用 GC 的緣由是由於會阻塞當前線程並引發沒必要要的 CPU 週期,若是 GC 致使其它線程被搶佔的話,顯式 GC 還會引起 jank(jank是指第 n 幀繪製事後,本該繪製第 n+1 幀,但由於 CPU 被搶佔,數據沒有準備好,只好再顯示一次第 n 幀,下一次繪製時顯示第 n+1);
        • NativeAlloc
        • 來自 native 分配的 native memory 壓力引發的 GC,好比 Bitmap 或者 RenderScript 對象;
        • CollectorTransition
        • heap 變遷引發的 GC,運行時動態切換 GC 形成的,垃圾回收器變遷過程包括從 free-list backed space 複製全部對象到 bump pointer space(反之亦然),當前垃圾回收器過渡只會在低 RAM 設備的 app 改變運行狀態時發生,好比從可察覺的停頓態到非可察覺的停頓態(反之亦然);
        • HomogeneousSpaceCompact
        • HomogeneousSpaceCompact 指的是 free-list space 空間的壓縮,常常在 app 變成不可察覺的停頓態時發生,這樣作的主要緣由是減小 RAM 佔用並整理 heap 碎片;
        • DisableMovingGc
        • 不是一個真正的 GC 緣由,正在整理碎片的 GC 被 GetPrimitiveArrayCritical 阻塞,通常來講由於 GetPrimitiveArrayCritical 會限制垃圾回收器內存移動,強烈建議不要使用;
        • HeapTrim
        • 不是一個真正的 GC 緣由,僅僅是一個收集器被阻塞直到堆壓縮完成的記錄。

      • GC Name:ART有幾種不一樣的GC
        • Concurrent mark sweep (CMS)
        • 全堆垃圾收集器,負責收集釋放除 image space(上面 ART 堆的圖片中對應區域)外的全部空間;
        • Concurrent partial mark sweep
        • 差很少是全堆垃圾收集器,負責收集除 image space 和 zygote space 外的全部空間;
        • Concurrent sticky mark sweep
        • 分代垃圾收集器,只負責釋放從上次 GC 到如今分配的對象,該 GC 比全堆和部分標記清除執行得更頻繁,由於它更快並且停頓更短;
        • Marksweep + semispace
        • 非同步的,堆拷貝壓縮和 HomogeneousSpaceCompaction 同時執行。

      • Objects freed
      • 本次 GC 從非大對象空間(non large object space)回收的對象數目。
      • Size freed
      • 本次 GC 從非大對象空間回收的字節數。
      • Large objects freed
      • 本次 GC 從大對象空間裏回收的對象數目。
      • Large object size freed
      • 本次GC從大對象空間裏回收的字節數。
      • Heap stats
      • 可用空間所佔的百分比和 [已使用內存大小] / [ heap 總大小]。
      • Pause times
      • 通常狀況下,GC 運行時停頓次數和被修改的對象引用數成比例,目前 ART CMS GC 只會在 GC 結束的時停頓一次,GC 過渡會有一個長停頓,是 GC 時耗的主要因素。

      Java/Android 引用解析

        GC 過程是和對象引用的類型是嚴重相關的,咱們在平時接觸到的通常有三種引用類型,強引用、軟引用、弱引用和虛引用:

      級別 回收時機 用途 生存時間
      強引用 歷來不會 對象的通常狀態 Cool
      軟引用 在內存不足的時候 聯合 ReferenceQueue 構造有效期短/佔內存大/生命週期長的對象的二級高速緩衝器(內存不足時才清空) 內存不足時終止
      弱引用 在垃圾回收時 聯合 ReferenceQueue 構造有效期短/佔內存大/生命週期長的對象的一級高速緩衝器(系統發生GC則清空) GC 運行後終止
      虛引用 在垃圾回收時 聯合 ReferenceQueue 來跟蹤對象被垃圾回收器回收的活動 GC 運行後終止

      在 Java/Android 開發中,爲了防止內存溢出,在處理一些佔內存大並且生命週期比較長對象的時候,能夠儘可能應用軟引用和弱引用,軟/弱引用能夠和一個引用隊列(ReferenceQueue)聯合使用,若是軟引用所引用的對象被垃圾回收器回收,Java 虛擬機就會把這個軟引用加入到與之關聯的引用隊列中,利用這個隊列能夠得知被回收的軟/弱引用的對象列表,從而爲緩衝器清除已失效的軟/弱引用。

      Android 內存泄漏和優化

        具體的請看中篇:Android 性能優化以內存泄漏檢測以及內存優化(中)和下篇:Android 性能優化以內存泄漏檢測以及內存優化(下)

      引用

      blog.csdn.net/luoshengyan…
      blog.csdn.net/luoshengyan…
      blog.csdn.net/luoshengyan…
      blog.csdn.net/luoshengyan…
      blog.csdn.net/luoshengyan…
      mp.weixin.qq.com/s?__biz=MzA…
      geek.csdn.net/news/detail…
      www.jianshu.com/p/216b03c22…
      zhuanlan.zhihu.com/p/25213586
      joyrun.github.io/2016/08/08/…
      www.cnblogs.com/larack/p/60…
      source.android.com/devices/tec…
      blog.csdn.net/high2011/ar…
      gityuan.com/2015/10/03/…
      www.ayqy.net/blog/androi…
      developer.android.com/studio/prof…

      相關文章
      相關標籤/搜索