Android 效能優化之記憶體洩漏檢測以及記憶體優化(上)

Shawn_Dut發表於2017-04-10

  在 Java 中,記憶體的分配是由程式完成的,而記憶體的釋放則是由 Garbage Collecation(GC) 完成的,Java/Android 程式設計師不用像 C/C++ 程式設計師一樣手動呼叫相關函式來管理記憶體的分配和釋放,雖然方便了很多,但是這也就造成了記憶體洩漏的可能性,所以記錄一下針對 Android 應用的記憶體洩漏的檢測,處理和優化的相關內容,上篇主要會分析 Java/Android 的記憶體分配以及 GC 的詳細分析,中篇會闡述 Android 記憶體洩漏的檢測和記憶體洩漏的常見產生情景,下篇會分析一下記憶體優化的內容。
  上篇:Android 效能優化之記憶體洩漏檢測以及記憶體優化(上)
  中篇:Android 效能優化之記憶體洩漏檢測以及記憶體優化(中)
  下篇:Android 效能優化之記憶體洩漏檢測以及記憶體優化(下)
  轉載請註明出處:blog.csdn.net/self_study/…
  對技術感興趣的同鞋加群544645972一起交流。

Java/Android 記憶體分配和回收策略分析

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

Java/Android 記憶體分配策略

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

  • 靜態儲存區(方法區)
  • 記憶體在程式編譯的時候就已經分配好,這塊記憶體在程式整個執行期間都存在,它主要是用來存放靜態資料、全域性 static 資料和常量;
  • 棧區
  • 在執行函式時,函式內部區域性變數的儲存單元都可以在棧上建立,函式執行結束時這些儲存單元自動被釋放,棧記憶體分配運算內建於處理器的指令集中,效率很高,但是分配的記憶體容量有限;
  • 堆區
  • 亦稱為動態記憶體分配,Java/Android 程式在適當的時候使用 new 關鍵字申請所需要大小的物件記憶體,然後通過 GC 決定在不需要這塊物件記憶體的時候回收它,但是由於我們的疏忽導致該物件在不需要繼續使用的之後,GC 仍然沒辦法回收該記憶體區域,這就代表發生了記憶體洩漏。
  堆區和棧區的區別:
  在函式中定義的一些基本型別的變數和物件的引用變數(也就是區域性變數的引用)都是在函式的棧記憶體分配的,當在一段程式碼塊中定義一個變數時,Java 就在棧中為這個變數分配記憶體空間,當超過變數的作用域後,Java 會自動釋放掉為該變數分配的記憶體空間,該記憶體空間可以立刻被重新使用;堆記憶體用於存放所有由 new 建立的物件(內容包括該物件其中的所有成員變數)和陣列,在堆中分配的記憶體是由 GC 來管理的,在堆中產生了一個物件或者陣列後,還可以在棧中生成一個引用指向這個堆中物件的記憶體區域,以後就可以通過棧中這個引用變數來訪問堆中的這個引用指向的物件或者陣列。下面這個圖片很好的說明了它兩的區別:
  
Android 效能優化之記憶體洩漏檢測以及記憶體優化(上)
示例圖片

  堆是不連續的記憶體區域(因為系統是用連結串列來儲存空閒記憶體地址,所以隨著記憶體的分配和釋放,肯定是不連續的),堆的大小受限於計算機系統中有效的虛擬記憶體(32bit 理論上是 4G),所以堆的空間比較大,也比較靈活;棧是一塊連續的記憶體區域,大小是作業系統預定好的,由於儲存的都是基本資料型別和物件的引用,所以大小一般不會太大,在幾 M 左右。上面的這些差異導致頻繁的記憶體申請和釋放造成堆記憶體在大量的碎片,使得堆的執行效率降低,而對於棧來說,它是先進後出的佇列,不產生碎片,執行效率高。
  綜上所述:
  • 區域性變數的基本資料型別和引用儲存於棧中,引用的物件實體儲存於堆中,因為它們屬於方法中的變數,生命週期隨方法而結束;
  • 成員變數全部儲存於堆中(包括基本資料型別,物件引用和引用指向的物件實體),因為它們屬於類,類物件終究是要被 new 出來使用的;
  • 我們所說的記憶體洩露,只針對堆記憶體,他們存放的就是引用指向的物件實體。

Java 常用垃圾回收機制

  

  • 引用計數
  • 比較古老的回收演算法,原理是此物件有一個引用,即增加一個計數,刪除一個引用則減少一個計數,垃圾回收時只用收集計數為 0 的物件,此演算法最致命的是無法處理迴圈引用的問題;
  • 標記-清除收集器
  • 這種收集器首先遍歷物件圖並標記可到達的物件,然後掃描堆疊以尋找未標記物件並釋放它們的記憶體,這種收集器一般使用單執行緒工作並會暫停其他執行緒操作,並且由於它只是清除了那些未標記的物件,而並沒有對標記物件進行壓縮,導致會產生大量記憶體碎片,從而浪費記憶體;
  • 標記-壓縮收集器
  • 有時也叫標記-清除-壓縮收集器,與標記-清除收集器有相同的標記階段,但是在第二階段則把標記物件複製到堆疊的新域中以便壓縮堆疊,這種收集器也會暫停其他操作;
  • 複製收集器(半空間)
  • 這種收集器將堆疊分為兩個域,常稱為半空間,每次僅使用一半的空間,JVM 生成的新物件則放在另一半空間中,GC 執行時它把可到達物件複製到另一半空間從而壓縮了堆疊,這種方法適用於短生存期的物件,持續複製長生存期的物件則導致效率降低,並且對於指定大小堆來說需要兩倍大小的記憶體,因為任何時候都只使用其中的一半;
  • 增量收集器
  • 增量收集器把堆疊分為多個域,每次僅從一個域收集垃圾,也可理解為把堆疊分成一小塊一小塊,每次僅對某一個塊進行垃圾收集,這就只會引起較小的應用程式中斷時間,使得使用者一般不能覺察到垃圾收集器執行;
  • 分代收集器
  • 複製收集器的缺點是每次收集時所有的標記物件都要被拷貝,從而導致一些生命週期很長的物件被來回拷貝多次,消耗大量的時間,而分代收集器則可解決這個問題,分代收集器把堆疊分為兩個或多個域用以存放不同壽命的物件,JVM 生成的新物件一般放在其中的某個域中,過一段時間,繼續存在的物件(非短命物件)將轉入更長壽命的域中,分代收集器對不同的域使用不同的演算法以優化效能。

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

Dalvik 堆簡介

  

Android 效能優化之記憶體洩漏檢測以及記憶體優化(上)
這裡寫圖片描述

上圖為 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 分配記憶體過程分析

  

Android 效能優化之記憶體洩漏檢測以及記憶體優化(上)
這裡寫圖片描述

上圖就是 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 中列印一條對應的日誌,我們只需要去分析這條日誌就可以了,日誌的基本格式如下:

    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 堆簡介

        

      Android 效能優化之記憶體洩漏檢測以及記憶體優化(上)
      這裡寫圖片描述

      上圖為 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 分配記憶體過程分析

        

      Android 效能優化之記憶體洩漏檢測以及記憶體優化(上)
      這裡寫圖片描述

      上圖就是 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 物件的成員變數 objectsize 中。 Heap 類的成員函式 AllocObject 首先是要確定要在哪個 Space 上分配記憶體,可以分配記憶體的 Space 有三個,分別 Zygote Space、Allocation Space 和 Large Object Space,不過 Zygote Space 在還沒有劃分出 Allocation Space 之前就在 Zygote Space 上分配,而當 Zygote Space 劃分出 Allocation Space 之後,就只能在 Allocation Space 上分配,同時 Heap 類的成員變數 allocspace 在 Zygote Space 還沒有劃分出 Allocation Space 之前指向 Zygote Space,而劃分之後就指向 Allocation Space,Large Object Space 則始終由 Heap 類的成員變數 large_objectspace 指向。只要滿足以下三個條件就在 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,它們的實現如下所示:

      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 時,可以最大限度地減少應用程式停頓:

      Android 效能優化之記憶體洩漏檢測以及記憶體優化(上)
      這裡寫圖片描述

      上圖描述了 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 的執行過程圖:

      Android 效能優化之記憶體洩漏檢測以及記憶體優化(上)
      Semi-Space compaction

      Semi-Space compaction

      Android 效能優化之記憶體洩漏檢測以及記憶體優化(上)
      這裡寫圖片描述

      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…

      相關文章