Android 性能優(yōu)化之內(nèi)存泄漏檢測以及內(nèi)存優(yōu)化(上)

在 Java 中,內(nèi)存的分配是由程序完成的,而內(nèi)存的釋放則是由 Garbage Collecation(GC) 完成的,Java/Android 程序員不用像 C/C++ 程序員一樣手動調(diào)用相關(guān)函數(shù)來管理內(nèi)存的分配和釋放,雖然方便了很多,但是這也就造成了內(nèi)存泄漏的可能性,所以記錄一下針對 Android 應(yīng)用的內(nèi)存泄漏的檢測,處理和優(yōu)化的相關(guān)內(nèi)容,上篇主要會分析 Java/Android 的內(nèi)存分配以及 GC 的詳細分析,中篇會闡述 Android 內(nèi)存泄漏的檢測和內(nèi)存泄漏的常見產(chǎn)生情景,下篇會分析一下內(nèi)存優(yōu)化的內(nèi)容。
  上篇:Android 性能優(yōu)化之內(nèi)存泄漏檢測以及內(nèi)存優(yōu)化(上)
  中篇:Android 性能優(yōu)化之內(nèi)存泄漏檢測以及內(nèi)存優(yōu)化(中)
  下篇:Android 性能優(yōu)化之內(nèi)存泄漏檢測以及內(nèi)存優(yōu)化(下)
  轉(zhuǎn)載請注明出處:http://blog.csdn.net/self_study/article/details/61919483
  對技術(shù)感興趣的同鞋加群544645972一起交流。

Java/Android 內(nèi)存分配和回收策略分析

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

Java/Android 內(nèi)存分配策略

Java/Android 程序運行時的內(nèi)存分配有三種策略,分別是靜態(tài)的,棧式的和堆式的,對應(yīng)的三種存儲策略使用的內(nèi)存空間主要分別是靜態(tài)存儲區(qū)(方法區(qū))、堆區(qū)和棧區(qū):<ul><li>靜態(tài)存儲區(qū)(方法區(qū))</li>內(nèi)存在程序編譯的時候就已經(jīng)分配好,這塊內(nèi)存在程序整個運行期間都存在,它主要是用來存放靜態(tài)數(shù)據(jù)、全局 static 數(shù)據(jù)和常量;<li>棧區(qū)</li>在執(zhí)行函數(shù)時,函數(shù)內(nèi)部局部變量的存儲單元都可以在棧上創(chuàng)建,函數(shù)執(zhí)行結(jié)束時這些存儲單元自動被釋放,棧內(nèi)存分配運算內(nèi)置于處理器的指令集中,效率很高,但是分配的內(nèi)存容量有限;<li>堆區(qū)</li>亦稱為動態(tài)內(nèi)存分配,Java/Android 程序在適當?shù)臅r候使用 new 關(guān)鍵字申請所需要大小的對象內(nèi)存,然后通過 GC 決定在不需要這塊對象內(nèi)存的時候回收它,但是由于我們的疏忽導(dǎo)致該對象在不需要繼續(xù)使用的之后,GC 仍然沒辦法回收該內(nèi)存區(qū)域,這就代表發(fā)生了內(nèi)存泄漏。</ul>  堆區(qū)和棧區(qū)的區(qū)別:
  在函數(shù)中定義的一些基本類型的變量和對象的引用變量(也就是局部變量的引用)都是在函數(shù)的棧內(nèi)存分配的,當在一段代碼塊中定義一個變量時,Java 就在棧中為這個變量分配內(nèi)存空間,當超過變量的作用域后,Java 會自動釋放掉為該變量分配的內(nèi)存空間,該內(nèi)存空間可以立刻被重新使用;堆內(nèi)存用于存放所有由 new 創(chuàng)建的對象(內(nèi)容包括該對象其中的所有成員變量)和數(shù)組,在堆中分配的內(nèi)存是由 GC 來管理的,在堆中產(chǎn)生了一個對象或者數(shù)組后,還可以在棧中生成一個引用指向這個堆中對象的內(nèi)存區(qū)域,以后就可以通過棧中這個引用變量來訪問堆中的這個引用指向的對象或者數(shù)組。下面這個圖片很好的說明了它兩的區(qū)別:
  

示例圖片

  堆是不連續(xù)的內(nèi)存區(qū)域(因為系統(tǒng)是用鏈表來存儲空閑內(nèi)存地址,所以隨著內(nèi)存的分配和釋放,肯定是不連續(xù)的),堆的大小受限于計算機系統(tǒng)中有效的虛擬內(nèi)存(32bit 理論上是 4G),所以堆的空間比較大,也比較靈活;棧是一塊連續(xù)的內(nèi)存區(qū)域,大小是操作系統(tǒng)預(yù)定好的,由于存儲的都是基本數(shù)據(jù)類型和對象的引用,所以大小一般不會太大,在幾 M 左右。上面的這些差異導(dǎo)致頻繁的內(nèi)存申請和釋放造成堆內(nèi)存在大量的碎片,使得堆的運行效率降低,而對于棧來說,它是先進后出的隊列,不產(chǎn)生碎片,運行效率高。
  綜上所述:<ul><li>局部變量的基本數(shù)據(jù)類型和引用存儲于棧中,引用的對象實體存儲于堆中,因為它們屬于方法中的變量,生命周期隨方法而結(jié)束;</li><li>成員變量全部存儲于堆中(包括基本數(shù)據(jù)類型,對象引用和引用指向的對象實體),因為它們屬于類,類對象終究是要被 new 出來使用的;</li><li>我們所說的內(nèi)存泄露,只針對堆內(nèi)存,他們存放的就是引用指向的對象實體。</li></ul>

Java 常用垃圾回收機制

<ul><li>引用計數(shù)</li>比較古老的回收算法,原理是此對象有一個引用,即增加一個計數(shù),刪除一個引用則減少一個計數(shù),垃圾回收時只用收集計數(shù)為 0 的對象,此算法最致命的是無法處理循環(huán)引用的問題;<li>標記-清除收集器</li>這種收集器首先遍歷對象圖并標記可到達的對象,然后掃描堆棧以尋找未標記對象并釋放它們的內(nèi)存,這種收集器一般使用單線程工作并會暫停其他線程操作,并且由于它只是清除了那些未標記的對象,而并沒有對標記對象進行壓縮,導(dǎo)致會產(chǎn)生大量內(nèi)存碎片,從而浪費內(nèi)存;<li>標記-壓縮收集器</li>有時也叫標記-清除-壓縮收集器,與標記-清除收集器有相同的標記階段,但是在第二階段則把標記對象復(fù)制到堆棧的新域中以便壓縮堆棧,這種收集器也會暫停其他操作;<li>復(fù)制收集器(半空間)</li>這種收集器將堆棧分為兩個域,常稱為半空間,每次僅使用一半的空間,JVM 生成的新對象則放在另一半空間中,GC 運行時它把可到達對象復(fù)制到另一半空間從而壓縮了堆棧,這種方法適用于短生存期的對象,持續(xù)復(fù)制長生存期的對象則導(dǎo)致效率降低,并且對于指定大小堆來說需要兩倍大小的內(nèi)存,因為任何時候都只使用其中的一半;<li>增量收集器</li>增量收集器把堆棧分為多個域,每次僅從一個域收集垃圾,也可理解為把堆棧分成一小塊一小塊,每次僅對某一個塊進行垃圾收集,這就只會引起較小的應(yīng)用程序中斷時間,使得用戶一般不能覺察到垃圾收集器運行;<li>分代收集器</li>復(fù)制收集器的缺點是每次收集時所有的標記對象都要被拷貝,從而導(dǎo)致一些生命周期很長的對象被來回拷貝多次,消耗大量的時間,而分代收集器則可解決這個問題,分代收集器把堆棧分為兩個或多個域用以存放不同壽命的對象,JVM 生成的新對象一般放在其中的某個域中,過一段時間,繼續(xù)存在的對象(非短命對象)將轉(zhuǎn)入更長壽命的域中,分代收集器對不同的域使用不同的算法以優(yōu)化性能。</ul>

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

Dalvik 堆簡介

這里寫圖片描述

上圖為 Dalvik 虛擬機的 Java 堆描述(出自:Dalvik虛擬機Java堆創(chuàng)建過程分析),如上圖所示,在 Dalvik 虛擬機中,Java 堆實際上是由一個 Active 堆和一個 Zygote 堆組成的,其中 Zygote 堆用來管理 Zygote 進程在啟動過程中預(yù)加載和創(chuàng)建的各種對象,而 Active 堆是在 Zygote 進程 fork 第一個子進程之前創(chuàng)建的,應(yīng)用進程都是通過 Zygote 進程 fork 出來的(相關(guān)函數(shù)為 ZygoteInit.main 函數(shù):Android TransactionTooLargeException 解析,思考與監(jiān)控方案),之后無論是 Zygote 進程還是其子進程,都在 Active 堆上進行對象分配和釋放,這樣做的目的是使得 Zygote 進程和其子進程最大限度地共享 Zygote 堆所占用的內(nèi)存。上面講到應(yīng)用程序進程是由 Zygote 進程 fork 出來的,也就是說應(yīng)用程序進程使用了一種寫時拷貝技術(shù)(COW)來復(fù)制 Zygote 進程的地址空間,這意味著一開始的時候,應(yīng)用程序進程和 Zygote 進程共享了同一個用來分配對象的堆,然而當 Zygote 進程或者應(yīng)用程序進程對該堆進行寫操作時,內(nèi)核才會執(zhí)行真正的拷貝操作,使得 Zygote 進程和應(yīng)用程序進程分別擁有自己的一份拷貝。拷貝是一件費時費力的事情,因此為了盡量地避免拷貝,Dalvik 虛擬機將自己的堆劃分為兩部分,事實上 Dalvik 虛擬機的堆最初是只有一個的,也就是 Zygote 進程在啟動過程中創(chuàng)建 Dalvik 虛擬機的時候只有一個堆,但是當 Zygote 進程在 fork 第一個應(yīng)用程序進程之前會將已經(jīng)使用了的那部分堆內(nèi)存劃分為一部分,還沒有使用的堆內(nèi)存劃分為另外一部分,前者就稱為 Zygote 堆,后者就稱為 Active 堆。以后無論是 Zygote 進程還是應(yīng)用程序進程,當它們需要分配對象的時候,都在 Active 堆上進行,這樣就可以使得 Zygote 堆被應(yīng)用進程和 Zygote 進程共享從而盡可能少地被執(zhí)行寫操作,所以就可以減少執(zhí)行寫時的拷貝操作。在 Zygote 堆里面分配的對象其實主要就是 Zygote 進程在啟動過程中預(yù)加載的類、資源和對象,這意味著這些預(yù)加載的類、資源和對象可以在 Zygote 進程和應(yīng)用程序進程中做到長期共享,這樣既能減少拷貝操作還能減少對內(nèi)存的需求(出自:Dalvik虛擬機垃圾收集機制簡要介紹和學(xué)習(xí)計劃)。

Dalvik 分配內(nèi)存過程分析

這里寫圖片描述

上圖就是 Dalvik VM 為新創(chuàng)建對象分配內(nèi)存的過程(出自:Dalvik虛擬機為新創(chuàng)建對象分配內(nèi)存的過程分析),我們來看看分配的具體步驟:<ol><li>Dalvik 虛擬機實現(xiàn)了一個 dvmAllocObject 函數(shù),每當 Dalvik 虛擬機需要為對象分配內(nèi)存時,就會調(diào)用函數(shù) dvmAllocObject,例如,當 Dalvik 虛擬機的解釋器遇到一個 new 指令時,它就會調(diào)用函數(shù) dvmAllocObject;</li><li>函數(shù) dvmAllocObject 調(diào)用函數(shù) dvmMalloc 從 Java 堆中分配一塊指定大小的內(nèi)存給新創(chuàng)建的對象使用,如果分配成功,那么接下來就先使用宏 DVM_OBJECT_INIT 來初始化新創(chuàng)建對象的成員變量 clazz,使得新創(chuàng)建的對象可以與某個特定的類關(guān)聯(lián)起來,接著再調(diào)用函數(shù) dvmTrackAllocation 記錄當前的內(nèi)存分配信息,以便通知 DDMS。函數(shù) dvmMalloc 返回的只是一塊內(nèi)存地址,這是沒有類型的,但是由于每一個 Java 對象都是從 Object 類繼承下來的,因此函數(shù) dvmAllocObject 可以將獲得的沒有類型的內(nèi)存塊強制轉(zhuǎn)換為一個 Object 對象;</li><li> dvmMalloc 函數(shù)接著調(diào)用到了另一個函數(shù) tryMalloc ,真正執(zhí)行內(nèi)存分配操作的就是這個 tryMalloc 函數(shù),dvmMalloc 函數(shù)操作如果分配內(nèi)存成功,則記錄當前線程成功分配的內(nèi)存字節(jié)數(shù)和對象數(shù)等信息;否則的話,就記錄當前線程失敗分配的內(nèi)存字節(jié)數(shù)和對象等信息,方便通過 DDMS 等工具對內(nèi)存使用信息進行統(tǒng)計,同時會調(diào)用函數(shù) throwOOME 拋出一個 OOM 異常;</li>

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;  
}  

<li>再來具體分析一下函數(shù) tryMalloc,tryMalloc 會調(diào)用函數(shù) dvmHeapSourceAlloc 在 Java 堆上分配指定大小的內(nèi)存,如果分配成功,那么就將分配得到的地址直接返回給調(diào)用者了,函數(shù) dvmHeapSourceAlloc 在不改變 Java 堆當前大小的前提下進行內(nèi)存分配,這是屬于輕量級的內(nèi)存分配動作;</li><li>如果上一步內(nèi)存分配失敗,這時候就需要執(zhí)行一次 GC 了,不過如果 GC 線程已經(jīng)在運行中,即 gDvm.gcHeap->gcRunning 的值等于 true,那么就直接調(diào)用函數(shù) dvmWaitForConcurrentGcToComplete 等到 GC 執(zhí)行完成;否則的話,就需要調(diào)用函數(shù) gcForMalloc 來執(zhí)行一次 GC 了,參數(shù) false 表示不要回收軟引用對象引用的對象;</li>

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;  
}  

<li>GC 執(zhí)行完畢后,再次調(diào)用函數(shù) dvmHeapSourceAlloc 嘗試輕量級的內(nèi)存分配操作,如果分配成功,那么就將分配得到的地址直接返回給調(diào)用者了;</li><li>如果上一步內(nèi)存分配失敗,這時候就得考慮先將 Java 堆的當前大小設(shè)置為 Dalvik 虛擬機啟動時指定的 Java 堆最大值再進行內(nèi)存分配了,這是通過調(diào)用函數(shù) dvmHeapSourceAllocAndGrow 來實現(xiàn)的;</li><li>如果調(diào)用函數(shù) dvmHeapSourceAllocAndGrow 分配內(nèi)存成功,則直接將分配得到的地址直接返回給調(diào)用者了;</li><li>如果上一步內(nèi)存分配還是失敗,這時候就得出狠招了,再次調(diào)用函數(shù) gcForMalloc 來執(zhí)行 GC,不過這次參數(shù)為 true 表示要回收軟引用對象引用的對象;</li><li>上一步 GC 執(zhí)行完畢,再次調(diào)用函數(shù) dvmHeapSourceAllocAndGrow 進行內(nèi)存分配,這是最后一次努力了,如果還分配內(nèi)存不成功那就是 OOM 了。</li></ol>

Dalvik GC 策略分析

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

  
這里寫圖片描述

圖中的每個圓節(jié)點代表對象的內(nèi)存資源,箭頭代表可達路徑,當一個圓節(jié)點和 GC Roots 存在可達的路徑時,表示當前它指向的內(nèi)存資源正在被引用,虛擬機是無法對其進行回收的(圖中的黃色節(jié)點);反過來,如果當前的圓節(jié)點和 GC Roots 不存在可達路徑,則意味著這塊對象的內(nèi)存資源不再被程序引用,系統(tǒng)虛擬機可以在 GC 的時候?qū)⑵鋬?nèi)存回收掉。具體點來說,Java/Android 的內(nèi)存垃圾回收機制是從程序的主要運行對象(如靜態(tài)對象/寄存器/棧上指向的內(nèi)存對象等,對應(yīng)上面的 GC Roots)開始檢查調(diào)用鏈,當遍歷一遍后得到上述這些無法回收的對象和他們所引用的對象鏈組成無法回收的對象集合,而剩余其他的孤立對象(集)就作為垃圾被 GC 回收。GC 為了能夠正確釋放對象,必須監(jiān)控每一個對象的運行狀態(tài),包括對象的申請、引用、被引用、賦值等。監(jiān)視對象狀態(tài)是為了更加準確地、及時地釋放對象,而釋放對象的根本原則就是該對象不再被引用。

  上面介紹了 GC 的回收機制,那么接下來據(jù)此說一下什么是內(nèi)存泄漏,從抽象定義上講,Java/Android 平臺的內(nèi)存泄漏是指沒有用的對象資源仍然和 GC Roots 保持可達路徑,導(dǎo)致系統(tǒng)無法進行回收,具體一點講就是,通過 GC Roots 的調(diào)用鏈可以遍歷到這個沒有被使用的對象,導(dǎo)致該資源無法進行釋放。最常見的比如,Android 中的 Activity 中創(chuàng)建一個內(nèi)部類 Handler 用來處理多線程的消息,而內(nèi)部類會持有外部類的引用,所以該 Handler 的對象會持有 Activity 的引用,而如果這個 Handler 對象被子線程持有,子線程正在進行耗時的操作沒法在短時間內(nèi)執(zhí)行完成,那么一系列的引用鏈導(dǎo)致 Activity 關(guān)閉之后一直無法被釋放,重復(fù)地打開關(guān)閉這個 Activity 會造成這些 Activity 的對象一直在內(nèi)存當中,最終達到一定程度之后會產(chǎn)生 OOM 異常。
  在 Java/Android 中,雖然我們有幾個函數(shù)可以訪問 GC,例如運行GC的函數(shù) System.gc(),但是根據(jù) Java 語言規(guī)范定義,該函數(shù)不保證 JVM 的垃圾收集器一定會馬上執(zhí)行。因為不同的 JVM 實現(xiàn)者可能使用不同的算法管理 GC,通常 GC 的線程的優(yōu)先級別較低。JVM 調(diào)用 GC 的策略也有很多種,有的是內(nèi)存使用到達一定程度時 GC 才開始工作,也有定時執(zhí)行的,有的是平緩執(zhí)行GC,也有的是中斷式執(zhí)行GC,但通常來說我們開發(fā)者不需要關(guān)心這些。

Dalvik GC 日志分析

上面介紹到,雖然我們有幾個函數(shù)可以訪問 GC,但是該函數(shù)不會保證 GC 操作會立馬執(zhí)行,那么我怎么去監(jiān)聽系統(tǒng)的 GC 過程來實時分析當前的內(nèi)存狀態(tài)呢?其實很簡單,Android 4.4 版本之下系統(tǒng) Dalvik 每進行一次 GC 操作都會在 LogCat 中打印一條對應(yīng)的日志,我們只需要去分析這條日志就可以了,日志的基本格式如下:

D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>,  <Pause_time>

這段日志分為 4 個部分:<ul>
<li>首先是第一部分 GC_Reason,就是觸發(fā)這次 GC 的原因,一般情況下有以下幾種原因:</li><ul><li> GC_CONCURRENT </li>當我們應(yīng)用程序的堆內(nèi)存快要滿的時候,系統(tǒng)會自動觸發(fā) GC 操作來釋放內(nèi)存;<li> GC_FOR_MALLOC </li>當我們的應(yīng)用程序需要分配更多內(nèi)存,可是現(xiàn)有內(nèi)存已經(jīng)不足的時候,系統(tǒng)會進行 GC 操作來釋放內(nèi)存;<li> GC_HPROF_DUMP_HEAP </li>當生成內(nèi)存分析 HPROF 文件的時候,系統(tǒng)會進行 GC 操作,我們下面會分析一下 HPROF 文件;<li> GC_EXPLICIT </li>這種情況就是我們剛才提到過的,主動通知系統(tǒng)去進行GC操作,比如調(diào)用 System.gc() 方法來通知系統(tǒng),或者在 DDMS 中,通過工具按鈕也是可以顯式地告訴系統(tǒng)進行 GC 操作的。</ul>
<li>第二部分 Amount_freed,表示系統(tǒng)通過這次 GC 操作釋放了多少的內(nèi)存;</li>
<li>第三部分 Heap_stats,代表當前內(nèi)存的空閑比例以及使用情況(活動對象所占內(nèi)存 / 當前程序總內(nèi)存);</li>
<li>第四部分 Pause_time,代表了這次 GC 操作導(dǎo)致應(yīng)用程序暫停的時間,在 Android 2.3 版本之前 GC 操作是不能并發(fā)執(zhí)行的,所以如果當系統(tǒng)正在 GC 的時候,應(yīng)用程序只能阻塞等待 GC 結(jié)束,GC 的時間基本在幾百毫秒左右,所以用戶會感覺到略微明顯的卡頓,體驗不好,在 Android 2.3 以及之后到 4.4 版本之前,Dalvik GC 的操作改成了并發(fā)執(zhí)行,也就是說 GC 的操作不會影響到主應(yīng)用程序的正常運行,但是 GC 操作的開始和結(jié)束仍然會短暫的阻塞一段時間,不過時間上面就已經(jīng)短到讓用戶無法察覺到了。</li>
</ul>

Android 4.4 及以上 ART 分析

ART 堆簡介

這里寫圖片描述

上圖為 ART 堆的描述(圖片出自:ART運行時垃圾收集機制簡要介紹和學(xué)習(xí)計劃),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 是在地址上連續(xù)的空間,稱為 Continuous Space,而 Large Object Space 是一些離散地址的集合,用來分配一些大對象,稱為 Discontinuous Space。
  在 Image Space 和 Zygote Space 之間,隔著一段用來映射 system@framework@boot.art@classes.oat 文件的內(nèi)存,system@framework@boot.art@classes.oat 是一個 OAT 文件,它是由在系統(tǒng)啟動類路徑中的所有 .dex 文件翻譯得到的,而 Image Space 空間就包含了那些需要預(yù)加載的系統(tǒng)類對象,這意味著需要預(yù)加載的類對象是在生成 system@framework@boot.art@classes.oat 這個 OAT 文件的時候創(chuàng)建并且保存在文件 system@framework@boot.art@classes.dex 中,以后只要系統(tǒng)啟動類路徑中的 .dex 文件不發(fā)生變化(即不發(fā)生更新升級),那么以后每次系統(tǒng)啟動只需要將文件 system@framework@boot.art@classes.dex 直接映射到內(nèi)存即可,省去了創(chuàng)建各個類對象的時間。之前使用 Dalvik 虛擬機作為應(yīng)用程序運行時的時候,每次系統(tǒng)啟動都需要為那些預(yù)加載的系統(tǒng)類創(chuàng)建類對象,而雖然 ART 運行時第一次啟動會和 Dalvik 一樣比較慢,但是以后啟動實際上會快不少。由于 system@framework@boot.art@classes.dex 文件保存的是一些預(yù)先創(chuàng)建的對象,并且這些對象之間可能會互相引用,因此我們必須保證 system@framework@boot.art@classes.dex 文件每次加載到內(nèi)存的地址都是固定的,這個固定的地址保存在 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 進程和應(yīng)用程序進程之間共享的,而 Allocation Space 則是每個進程獨占的。同樣的 Zygote 進程一開始只有一個 Image Space 和一個 Zygote Space,在 Zygote 進程 fork 出第一個子進程之前,就會把 Zygote Space 一分為二,原來的已經(jīng)被使用的那部分堆還叫 Zygote Space,而未使用的那部分堆就叫 Allocation Space,以后的對象都在新分出來的 Allocation Space 上分配,通過上述這種方式,就可以使得 Image Space 和 Zygote Space 在 Zygote 進程和應(yīng)用程序進程之間進行共享,而 Allocation Space 就每個進程都獨立地擁有一份,和 Dalvik 同樣既能減少拷貝操作還能減少對內(nèi)存的需求。有一點需要注意的是雖然 Image Space 和 Zygote Space 都是在 Zygote 進程和應(yīng)用程序進程之間進行共享的,但是前者的對象只創(chuàng)建一次而后者的對象需要在系統(tǒng)每次啟動時根據(jù)運行情況都重新創(chuàng)建一遍(出自:ART運行時垃圾收集機制簡要介紹和學(xué)習(xí)計劃)。
  ART 運行時提供了兩種 Large Object Space 實現(xiàn),其中一種實現(xiàn)和 Continuous Space 的實現(xiàn)類似,預(yù)先分配好一塊大的內(nèi)存空間,然后再在上面為對象分配內(nèi)存塊,不過這種方式實現(xiàn)的 Large Object Space 不像 Continuous Space 通過 C 庫的內(nèi)塊管理接口來分配和釋放內(nèi)存,而是自己維護一個 Free List,每次為對象分配內(nèi)存時,都是從這個 Free List 找到合適的空閑的內(nèi)存塊來分配,釋放內(nèi)存的時候,也是將要釋放的內(nèi)存添加到該 Free List 去;另外一種 Large Object Space 實現(xiàn)是每次為對象分配內(nèi)存時,都單獨為其映射一新的內(nèi)存,也就是說,為每一個對象分配的內(nèi)存塊都是相互獨立的,這種實現(xiàn)方式相比上面介紹的 Free List 實現(xiàn)方式更簡單一些。在 Android 4.4 中,ART 運行時使用的是后一種實現(xiàn)方式,為每一對象映射一塊獨立的內(nèi)存塊的 Large Object Space 實現(xiàn)稱為 LargeObjectMapSpace,它與 Free List 方式的實現(xiàn)都是繼承于類 LargeObjectSpace,LargeObjectSpace 又分別繼承了 DiscontinuousSpace 和 AllocSpace,因此我們就可以知道,LargeObjectMapSpace 描述的是一個在地址空間上不連續(xù)的 Large Object Space。

ART 分配內(nèi)存過程分析

這里寫圖片描述

上圖就是 ART 為新創(chuàng)建對象分配內(nèi)存的過程(出自:ART運行時為新創(chuàng)建對象分配內(nèi)存的過程分析),可以看到 ART 為新創(chuàng)建對象分配內(nèi)存的過程和 Dalvik VM 幾乎是一樣的,區(qū)別僅僅在于垃圾收集的方式和策略不一樣。
  ART 運行時為從 DEX 字節(jié)碼翻譯得到的 Native 代碼提供的一個函數(shù)調(diào)用表中,有一個 pAllocObject 接口是用來分配對象的,當 ART 運行時以 Quick 模式運行在 ARM 體系結(jié)構(gòu)時,上述提到的 pAllocObject 接口由函數(shù) art_quick_alloc_object 來實現(xiàn),art_quick_alloc_object 是一段匯編代碼,最終經(jīng)過一系列的調(diào)用之后最終會調(diào)用 ART 運行時內(nèi)部的 Heap 對象的成員函數(shù) AllocObject 在堆上分配對象(具體的過程:ART運行時為新創(chuàng)建對象分配內(nèi)存的過程分析),其中要分配的大小保存在當前 Class 對象的成員變量 object_size_ 中。 Heap 類的成員函數(shù) AllocObject 首先是要確定要在哪個 Space 上分配內(nèi)存,可以分配內(nèi)存的 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 上分配:<ol><li>請求分配的內(nèi)存大于等于 Heap 類的成員變量 large_object_threshold_ 指定的值,這個值等于 3 * kPageSize,即 3 個頁面的大小;</li><li>已經(jīng)從 Zygote Space 劃分出 Allocation Space,即 Heap 類的成員變量 have_zygote_space_ 的值等于 true;</li><li>被分配的對象是一個原子類型數(shù)組,即 byte 數(shù)組、int 數(shù)組和 boolean 數(shù)組等。</li></ol>確定好要在哪個 Space 上分配內(nèi)存之后,就可以調(diào)用 Heap 類的成員函數(shù) Allocate 進行分配了,如果分配成功,Heap 類的成員函數(shù) Allocate 就返回新分配的對象并且將該對象保存在變量 obj 中,接下來再會做三件事情:<ol><li>調(diào)用 Object 類的成員函數(shù) SetClass 設(shè)置新分配對象 obj 的類型;</li><li>調(diào)用 Heap 類的成員函數(shù) RecordAllocation 記錄當前的內(nèi)存分配狀況;</li><li>檢查當前已經(jīng)分配出去的內(nèi)存是否已經(jīng)達到由 Heap 類的成員變量 concurrent_start_bytes_ 設(shè)定的閥值,如果已經(jīng)達到,那么就調(diào)用 Heap 類的成員函數(shù) RequestConcurrentGC 通知 GC 執(zhí)行一次并行 GC。</li></ol>另一方面如果 Heap 類的成員函數(shù) Allocate 分配內(nèi)存失敗,則 Heap 類的成員函數(shù) AllocObject 拋出一個 OOM 異常。Heap 類的 AllocObject 函數(shù)又會調(diào)用到成員函數(shù) 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;  
  }  
}  

函數(shù) Allocate 首先調(diào)用成員函數(shù) TryToAllocate 嘗試在不執(zhí)行 GC 的情況下進行內(nèi)存分配,如果分配失敗再調(diào)用成員函數(shù) AllocateInternalWithGc 進行帶 GC 的內(nèi)存分配,Allocate 是一個模板函數(shù),不同類型的 Space 會導(dǎo)致調(diào)用不同重載的成員函數(shù) TryToAllocate 進行不帶 GC 的內(nèi)存分配。雖然可以用來分配內(nèi)存的 Space 有 Zygote Space、Allocation Space 和 Large Object Space 三個,但是前兩者的類型是相同的,因此實際上只有兩個不同重載版本的成員函數(shù) TryToAllocate,它們的實現(xiàn)如下所示:

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 類兩個重載版本的成員函數(shù) TryToAllocate 的實現(xiàn)邏輯都幾乎是相同的,首先是調(diào)用另外一個成員函數(shù) IsOutOfMemoryOnAllocation 判斷分配請求的內(nèi)存后是否會超過堆的大小限制,如果超過則分配失敗;否則的話再在指定的 Space 進行內(nèi)存分配。函數(shù)IsOutOfMemoryOnAllocation的實現(xiàn)如下所示:

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_bytes_allocated_ 描述的是目前已經(jīng)分配出去的內(nèi)存字節(jié)數(shù),成員變量 max_allowed_footprint_ 描述的是目前堆可分配的最大內(nèi)存字節(jié)數(shù),成員變量 growth_limit_ 描述的是目前堆允許增長到的最大內(nèi)存字節(jié)數(shù),這里需要注意的一點是 max_allowed_footprint_ 是 Heap 類施加的一個限制,不會對各個 Space 實際可分配的最大內(nèi)存字節(jié)數(shù)產(chǎn)生影響,并且各個 Space 在創(chuàng)建的時候,已經(jīng)把自己可分配的最大內(nèi)存數(shù)設(shè)置為允許使用的最大內(nèi)存字節(jié)數(shù)。如果目前堆已經(jīng)分配出去的內(nèi)存字節(jié)數(shù)再加上請求分配的內(nèi)存字節(jié)數(shù) new_footprint 小于等于目前堆可分配的最大內(nèi)存字節(jié)數(shù) max_allowed_footprint_,那么分配出請求的內(nèi)存字節(jié)數(shù)之后不會造成 OOM,因此 Heap 類的成員函數(shù) IsOutOfMemoryOnAllocation 就返回false;另一方面,如果目前堆已經(jīng)分配出去的內(nèi)存字節(jié)數(shù)再加上請求分配的內(nèi)存字節(jié)數(shù) new_footprint 大于目前堆可分配的最大內(nèi)存字節(jié)數(shù) max_allowed_footprint_,并且也大于目前堆允許增長到的最大內(nèi)存字節(jié)數(shù) growth_limit_,那么分配出請求的內(nèi)存字節(jié)數(shù)之后造成 OOM,因此 Heap 類的成員函數(shù) IsOutOfMemoryOnAllocation 這時候就返回 true。
  剩下另外一種情況,目前堆已經(jīng)分配出去的內(nèi)存字節(jié)數(shù)再加上請求分配的內(nèi)存字節(jié)數(shù) new_footprint 大于目前堆可分配的最大內(nèi)存字節(jié)數(shù) max_allowed_footprint_,但是小于等于目前堆允許增長到的最大內(nèi)存字節(jié)數(shù) growth_limit_,這時候就要看情況會不會出現(xiàn) OOM 了:如果 ART 運行時運行在非并行 GC 的模式中,即 Heap 類的成員變量 concurrent_gc_ 等于 false,那么取決于允不允許增長堆的大小,即參數(shù) grow 的值,如果不允許,那么 Heap 類的成員函數(shù) IsOutOfMemoryOnAllocation 就返回 true,表示當前請求的分配會造成 OOM,如果允許,那么 Heap 類的成員函數(shù) IsOutOfMemoryOnAllocation 就會修改目前堆可分配的最大內(nèi)存字節(jié)數(shù) max_allowed_footprint_ 并且返回 false,表示允許當前請求的分配,這意味著在非并行 GC 運行模式中,如果分配內(nèi)存過程中遇到內(nèi)存不足并且當前可分配內(nèi)存還未達到增長上限時,要等到執(zhí)行完成一次非并行 GC 后才能成功分配到內(nèi)存,因為每次執(zhí)行完成 GC 之后都會按照預(yù)先設(shè)置的堆目標利用率來增長堆的大小;另一方面,如果 ART 運行時運行在并行 GC 的模式中,那么只要當前堆已經(jīng)分配出去的內(nèi)存字節(jié)數(shù)再加上請求分配的內(nèi)存字節(jié)數(shù) new_footprint 不超過目前堆允許增長到的最大內(nèi)存字節(jié)數(shù) growth_limit_,那么就不管允不允許增長堆的大小都認為不會發(fā)生 OOM,因此 Heap 類的成員函數(shù) IsOutOfMemoryOnAllocation 就返回 false,這意味著在并行 GC 運行模式中,在分配內(nèi)存過程中遇到內(nèi)存不足,并且當前可分配內(nèi)存還未達到增長上限時,無需等到執(zhí)行并行 GC 后就有可能成功分配到內(nèi)存,因為實際執(zhí)行內(nèi)存分配的 Space 可分配的最大內(nèi)存字節(jié)數(shù)是足夠的。

ART GC 策略以及過程分析

在 Android 4.4 版本以及之后就使用了 ART 運行時,在安裝的時候就將應(yīng)用翻譯成機器碼執(zhí)行,效率比起以前的 Dalvik 虛擬機更高,但是缺點就是安裝之后的應(yīng)用體積變大和安裝的時間會變長,不過相對于優(yōu)點來說,這點缺點不算什么。ART 運行時與 Dalvik 虛擬機一樣,都使用了 Mark-Sweep 算法進行垃圾回收,因此它們的垃圾回收流程在總體上是一致的,但是 ART 運行時對堆的劃分更加細致,因而在此基礎(chǔ)上實現(xiàn)了更多樣的回收策略。不同的策略有不同的回收力度,力度越大的回收策略每次回收的內(nèi)存就越多,并且它們都有各自的使用情景,這樣就可以使得每次執(zhí)行 GC 時,可以最大限度地減少應(yīng)用程序停頓:

這里寫圖片描述

上圖描述了 ART 運行時的垃圾收集收集過程(圖片出自:ART運行時垃圾收集(GC)過程分析),最上面三個箭頭描述觸發(fā) GC 的三種情況,左邊的流程圖描述非并行 GC 的執(zhí)行過程,右邊的流程圖描述并行 GC 的執(zhí)行流程,過程如下所示:<ul><li>非并行 GC :<ol><li>調(diào)用子類實現(xiàn)的成員函數(shù) InitializePhase 執(zhí)行 GC 初始化階段;</li><li>掛起所有的 ART 運行時線程;</li><li>調(diào)用子類實現(xiàn)的成員函數(shù) MarkingPhase 執(zhí)行 GC 標記階段;</li><li>調(diào)用子類實現(xiàn)的成員函數(shù) ReclaimPhase 執(zhí)行 GC 回收階段;</li><li>恢復(fù)第 2 步掛起的 ART 運行時線程;</li><li>調(diào)用子類實現(xiàn)的成員函數(shù) FinishPhase 執(zhí)行 GC 結(jié)束階段。</li></ol></li><li>并行 GC :<ol><li>調(diào)用子類實現(xiàn)的成員函數(shù) InitializePhase 執(zhí)行 GC 初始化階段;</li><li>獲取用于訪問 Java 堆的鎖;</li><li>調(diào)用子類實現(xiàn)的成員函數(shù) MarkingPhase 執(zhí)行 GC 并行標記階段;</li><li>釋放用于訪問 Java 堆的鎖;</li><li>掛起所有的 ART 運行時線程;</li><li>調(diào)用子類實現(xiàn)的成員函數(shù) HandleDirtyObjectsPhase 處理在 GC 并行標記階段被修改的對象;</li><li>恢復(fù)第 5 步掛起的 ART 運行時線程;</li><li>重復(fù)第 5 到第 7 步,直到所有在 GC 并行階段被修改的對象都處理完成;</li><li>獲取用于訪問 Java 堆的鎖;</li><li>調(diào)用子類實現(xiàn)的成員函數(shù) ReclaimPhase 執(zhí)行 GC 回收階段;</li><li>釋放用于訪問 Java 堆的鎖;</li><li>調(diào)用子類實現(xiàn)的成員函數(shù) FinishPhase 執(zhí)行 GC 結(jié)束階段。</li></ol></li></ul>它們的區(qū)別在于:<ol><li>非并行 GC 的標記階段和回收階段是在掛住所有的 ART 運行時線程的前提下進行的,因此只需要執(zhí)行一次標記即可;</li><li>并行 GC 的標記階段只鎖住了Java 堆,因此它不能阻止那些不是正在分配對象的 ART 運行時線程同時運行,而這些同時進運行的 ART 運行時線程可能會引用了一些在之前的標記階段沒有被標記的對象,如果不對這些對象進行重新標記的話,那么就會導(dǎo)致它們被 GC 回收造成錯誤,因此與非并行 GC 相比,并行 GC 多了一個處理臟對象的階段,所謂的臟對象就是我們前面說的在 GC 標記階段同時運行的 ART 運行時線程訪問或者修改過的對象;</li><li>并行 GC 并不是自始至終都是并行的,例如處理臟對象的階段就是需要掛起除 GC 線程以外的其它 ART 運行時線程,這樣才可以保證標記階段可以結(jié)束。</li></ol>
  上面 ART 堆內(nèi)存分配的時候,我們提到了有兩種可能會觸發(fā) GC 的情況,第一種情況是沒有足夠內(nèi)存分配給請求時,會調(diào)用 Heap 類的成員函數(shù) CollectGarbageInternal 觸發(fā)一個原因為 kGcCauseForAlloc 的 GC;第二種情況下分配出請求的內(nèi)存之后,堆剩下的內(nèi)存超過一定的閥值,就會調(diào)用 Heap 類的成員函數(shù) RequestConcurrentGC 請求執(zhí)行一個并行 GC;此外,還有第三種情況會觸發(fā)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);  
}  

當我們調(diào)用 Java 層的 java.lang.System 的靜態(tài)成員函數(shù) gc 時,如果 ART 運行時支持顯式 GC,那么它就會通過 JNI 調(diào)用 Heap 類的成員函數(shù) CollectGarbageInternal 來觸發(fā)一個原因為 kGcCauseExplicit 的 GC,ART 運行時默認是支持顯式 GC 的,但是可以通過啟動選項 -XX:+DisableExplicitGC 來關(guān)閉。所以 ART 運行時在三種情況下會觸發(fā) 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 類的成員函數(shù) 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;  
}  

參數(shù) gc_type 和 gc_cause 分別用來描述要執(zhí)行的 GC 的類型和原因,而參數(shù) clear_soft_references 用來描述是否要回收被軟引用指向的對象,Heap 類的成員函數(shù) CollectGarbageInternal 的執(zhí)行邏輯:<ol><li>通過一個 while 循環(huán)不斷地檢查 Heap 類的成員變量 is_gc_running_,直到它的值等于 false 為止,這表示當前沒有其它線程正在執(zhí)行 GC,當它的值等于 true 時就表示其它線程正在執(zhí)行 GC,這時候就要調(diào)用 Heap 類的成員函數(shù) WaitForConcurrentGcToComplete 等待其執(zhí)行完成,注意在當前 GC 執(zhí)行之前,Heap 類的成員變量 is_gc_running_ 會被設(shè)置為true;</li><li>如果當前請求執(zhí)行的 GC 類型為 kGcTypeSticky,但是當前 Allocation Space 的大小小于 Heap 類的成員變量 min_alloc_space_size_for_sticky_gc_ 指定的閥值,那么就改為執(zhí)行類型為 kGcTypePartial;</li><li>從 Heap 類的成員變量 mark_sweep_collectors_ 指向的一個垃圾收集器列表找到一個合適的垃圾收集器來執(zhí)行 GC,ART 運行時在內(nèi)部創(chuàng)建了六個垃圾收集器,這六個垃圾收集器分為兩組,一組支持并行 GC,另一組不支持;每一組都是由三個類型分別為 kGcTypeSticky、kGcTypePartial 和 kGcTypeFull 的垃垃圾收集器組成,這里說的合適的垃圾收集器是指并行性與 Heap 類的成員變量 concurrent_gc_ 一致,并且類型也與參數(shù) gc_type 一致的垃圾收集器;</li><li>找到合適的垃圾收集器之后,就將參數(shù) clear_soft_references 的值保存在它的成員變量 clear_soft_references_ 中,以便可以告訴它要不要回收被軟引用指向的對象,然后再調(diào)用它的成員函數(shù) Run 來執(zhí)行 GC;</li><li>GC 執(zhí)行完畢,將 Heap 類的成員變量 is_gc_running_ 設(shè)置為false,以表示當前 GC 已經(jīng)執(zhí)行完畢,下一次請求的 GC 可以執(zhí)行了,此外也會將 Heap 類的成員變量 last_gc_type_ 設(shè)置為當前執(zhí)行的 GC 的類型,這樣下一次執(zhí)行 GC 時,就可以執(zhí)行另外一個不同類型的 GC,例如如果上一次執(zhí)行的 GC 的類型為 kGcTypeSticky,那么接下來的兩次 GC 的類型就可以設(shè)置為 kGcTypePartial 和 kGcTypeFull,這樣可以使得每次都能執(zhí)行有效的 GC;</li><li>通過 Heap 類的成員變量 gc_complete_cond_ 喚醒那些正在等待 GC 執(zhí)行完成的線程。</li></ol>

ART GC 與 Dalvik GC 對比

?比起 Dalvik 的回收策略,ART 的 CMS(concurrent mark sweep,同步標記回收)有以下幾個優(yōu)點:<ul><li>阻塞的次數(shù)相比于 Dalvik 來說,從兩次減少到了一次,Dalvik 第一次的阻塞大部分工作是在標記 root,而在 ART CMS 中則是被每個執(zhí)行線程同步標記它們自己的 root 完成的,所以 ART 能夠立馬繼續(xù)運行;</li><li>和 Dalvik 類似,ART GC 同樣在回收執(zhí)行之前有一次暫停,但是關(guān)鍵的不同是 Dalvik 的一些執(zhí)行階段在 ART 中是并行同步執(zhí)行的,這些階段包括標記過程、系統(tǒng) weak Reference 清理過程(比如 jni weak globals 等)、重新標記非 GC Root 節(jié)點,Card 區(qū)域的提前清理。在 ART 中仍然需要阻塞的過程是掃描 Card 區(qū)域的臟數(shù)據(jù)和重新標記 GC Root,這兩個操作能夠降低阻塞的時間;</li><li>還有一個 ART GC 比 Dalvik 有提升的地方是 sticky CMS 提升了 GC 的吞吐量,不像正常的分代 GC 機制,sticky CMS 是不移動堆內(nèi)存的,它不會給新對象分配一個特定的區(qū)域(年輕代),新分配的對象被保存在一個分配棧里面,這個棧就是一個簡單的 Object 數(shù)組,這就避免了所需要移動對象的操作,也就獲得了低阻塞性,但是缺點就是會增加堆的對象復(fù)雜性;</li></ul>
  如果應(yīng)用程序在前臺運行時,這時候 GC 被稱為 Foreground GC,同時 ART 還有一個 Background GC,顧名思義就是在后臺運行的 GC,應(yīng)用程序在前臺運行時響應(yīng)性是最重要的,因此也要求執(zhí)行的 GC 是高效的,相反應(yīng)用程序在后臺運行時,響應(yīng)性不是最重要的,這時候就適合用來解決堆的內(nèi)存碎片問題,因此上面提到的所有 Mark-Sweep GC 適合作為 Foreground GC,而 Compacting GC(壓縮 GC) 適合作為 Background GC。當從 Foreground GC 切換到 Background GC,或者從 Background GC 切換到 Foreground GC,ActivityManager 會通知發(fā)生一次 Compacting GC 的行為,這是由于 Foreground GC 和 Background GC 的底層堆空間結(jié)構(gòu)是一樣的,因此發(fā)生 Foreground GC 和 Background GC 切換時,需要將當前存活的對象從一個 Space 轉(zhuǎn)移到另外一個 Space 上去,這個剛好就是 Semi-Space compaction 和 Homogeneous space compaction 適合干的事情。Background GC 壓縮內(nèi)存就能夠使得內(nèi)存碎片變少,從而達到縮減內(nèi)存的目的,但是壓縮內(nèi)存的時候會暫時阻塞應(yīng)用進程。 Semi-Space compaction 和 Homogeneous space compaction 有一個共同特點是都具有一個 From Space 和一個 To Space,在 GC 執(zhí)行期間,在 From Space 分配的還存活的對象會被依次拷貝到 To Space 中,這樣就可以達到消除內(nèi)存碎片的目的。與 Semi-Space compaction 相比,Homogeneous space compaction 還多了一個 Promote Space,當一個對象是在上一次 GC 之前分配的,并且在當前 GC 中仍然是存活的,那么它就會被拷貝到 Promote Space 而不是 To Space 中,這相當于是簡單地將對象劃分為新生代和老生代的,即在上一次 GC 之前分配的對象屬于老生代的,而在上一次 GC 之后分配的對象屬于新生代的,一般來說老生代對象的存活性要比新生代的久,因此將它們拷貝到 Promote Space 中去,可以避免每次執(zhí)行 Semi-Space compaction 或者 Homogeneous space compaction 時都需要對它們進行無用的處理,我們來看看這兩種 Background GC 的執(zhí)行過程圖:
<center>

Semi-Space compaction
</center>
<center>Semi-Space compaction</center>
<center>
這里寫圖片描述
</center>
<center>Homogeneous space compaction</center>
以上圖片來自:ART運行時Semi-Space(SS)和Generational Semi-Space(GSS)GC執(zhí)行過程分析,Bump Pointer Space 1 和 Bump Pointer Space 2 就是我們前面說的 From Space 和 To Space。Semi-Space compaction 一般發(fā)生在低內(nèi)存的設(shè)備上,而 Homogenous space compaction 是非低內(nèi)存設(shè)備上的默認壓縮模式。

GC Roots 解析

?GC Roots 特指的是垃圾收集器(Garbage Collector)的對象,GC 會收集那些不是 GC Roots 且沒有被 GC Roots 引用的對象,一個對象可以屬于多個 Root,GC Roots 有幾下種:<ul><li> Class </li>由系統(tǒng)類加載器(system class loader)加載的對象,這些類是不能夠被回收的,他們可以以靜態(tài)字段的方式持有其它對象。我們需要注意的一點就是,通過用戶自定義的類加載器加載的類,除非相應(yīng)的 java.lang.Class 實例以其它的某種(或多種)方式成為 Roots,否則它們并不是 Roots;<li> Thread </li>活著的線程;<li>Stack Local</li>Java 方法的 local 變量或參數(shù);<li>JNI Local</li>JNI 方法的 local 變量或參數(shù);<li>JNI Global</li>全局 JNI 引用;<li>Monitor Used</li>用于同步的監(jiān)控對象;<li>Held by JVM</li>用于 JVM 特殊目的由 GC 保留的對象,但實際上這個與 JVM 的實現(xiàn)是有關(guān)的,可能已知的一些類型是系統(tǒng)類加載器、一些 JVM 熟悉的重要異常類、一些用于處理異常的預(yù)分配對象以及一些自定義的類加載器等,然而 JVM 并沒有為這些對象提供其它的信息,因此就只有留給分析分員去確定哪些是屬于 "JVM 持有" 的了。</ul>來源:https://www.yourkit.com/docs/java/help/gc_roots.jsp

ART 日志分析

?ART 的 log 不同于 Dalvik 的 log 機制,不是明確調(diào)用的情況下不會打印的 GCs 的 log 信息,GC只會在被判定為很慢時輸出信息,更準確地說就是 GC 暫停的時間超過 5ms 或者 GC 執(zhí)行的總時間超過 100ms。如果 app 不是處于一種停頓可察覺的狀態(tài),那么 GC 就不會被判定為執(zhí)行緩慢,但是此時顯式 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

<ul>
<li>GC Reason :什么觸發(fā)了GC,以及屬于哪種類型的垃圾回收,可能出現(xiàn)的值包括<ul><li> Concurrent </li>并發(fā) GC,不會掛起 app 線程,這種 GC 在后臺線程中運行,不會阻止內(nèi)存分配;<li> Alloc </li>GC 被初始化,app 在 heap 已滿的時候請求分配內(nèi)存,此時 GC 會在當前線程(請求分配內(nèi)存的線程)執(zhí)行;<li> Explicit </li>GC 被 app 顯式請求,例如通過調(diào)用 System.gc() 或者 runtime.gc(),和 Dalvik 一樣,ART 建議相信 GC,盡可能地避免顯式調(diào)用 GC,不建議顯式調(diào)用 GC 的原因是因為會阻塞當前線程并引起不必要的 CPU 周期,如果 GC 導(dǎo)致其它線程被搶占的話,顯式 GC 還會引發(fā) jank(jank是指第 n 幀繪制過后,本該繪制第 n+1 幀,但因為 CPU 被搶占,數(shù)據(jù)沒有準備好,只好再顯示一次第 n 幀,下一次繪制時顯示第 n+1);<li> NativeAlloc </li>來自 native 分配的 native memory 壓力引起的 GC,比如 Bitmap 或者 RenderScript 對象;<li> CollectorTransition </li>heap 變遷引起的 GC,運行時動態(tài)切換 GC 造成的,垃圾回收器變遷過程包括從 free-list backed space 復(fù)制所有對象到 bump pointer space(反之亦然),當前垃圾回收器過渡只會在低 RAM 設(shè)備的 app 改變運行狀態(tài)時發(fā)生,比如從可察覺的停頓態(tài)到非可察覺的停頓態(tài)(反之亦然);<li> HomogeneousSpaceCompact </li>HomogeneousSpaceCompact 指的是 free-list space 空間的壓縮,經(jīng)常在 app 變成不可察覺的停頓態(tài)時發(fā)生,這樣做的主要原因是減少 RAM 占用并整理 heap 碎片;<li> DisableMovingGc </li>不是一個真正的 GC 原因,正在整理碎片的 GC 被 GetPrimitiveArrayCritical 阻塞,一般來說因為 GetPrimitiveArrayCritical 會限制垃圾回收器內(nèi)存移動,強烈建議不要使用;<li> HeapTrim </li>不是一個真正的 GC 原因,僅僅是一個收集器被阻塞直到堆壓縮完成的記錄。</ul></li>
<li> GC Name:ART有幾種不同的GC<ul><li>Concurrent mark sweep (CMS)</li>全堆垃圾收集器,負責收集釋放除 image space(上面 ART 堆的圖片中對應(yīng)區(qū)域)外的所有空間;<li>Concurrent partial mark sweep</li>差不多是全堆垃圾收集器,負責收集除 image space 和 zygote space 外的所有空間;<li>Concurrent sticky mark sweep</li>分代垃圾收集器,只負責釋放從上次 GC 到現(xiàn)在分配的對象,該 GC 比全堆和部分標記清除執(zhí)行得更頻繁,因為它更快而且停頓更短;<li>Marksweep + semispace</li>非同步的,堆拷貝壓縮和 HomogeneousSpaceCompaction 同時執(zhí)行。</ul></li>
<li>Objects freed</li>本次 GC 從非大對象空間(non large object space)回收的對象數(shù)目。
<li>Size freed</li>本次 GC 從非大對象空間回收的字節(jié)數(shù)。
<li>Large objects freed</li>本次 GC 從大對象空間里回收的對象數(shù)目。
<li>Large object size freed</li>本次GC從大對象空間里回收的字節(jié)數(shù)。
<li>Heap stats</li>可用空間所占的百分比和 [已使用內(nèi)存大小] / [ heap 總大小]。
<li>Pause times</li>一般情況下,GC 運行時停頓次數(shù)和被修改的對象引用數(shù)成比例,目前 ART CMS GC 只會在 GC 結(jié)束的時停頓一次,GC 過渡會有一個長停頓,是 GC 時耗的主要因素。
</ul>

Java/Android 引用解析

?GC 過程是和對象引用的類型是嚴重相關(guān)的,我們在平時接觸到的一般有三種引用類型,強引用、軟引用、弱引用和虛引用:

級別 回收時機 用途 生存時間
強引用 從來不會 對象的一般狀態(tài) Cool
軟引用 在內(nèi)存不足的時候 聯(lián)合 ReferenceQueue 構(gòu)造有效期短/占內(nèi)存大/生命周期長的對象的二級高速緩沖器(內(nèi)存不足時才清空) 內(nèi)存不足時終止
弱引用 在垃圾回收時 聯(lián)合 ReferenceQueue 構(gòu)造有效期短/占內(nèi)存大/生命周期長的對象的一級高速緩沖器(系統(tǒng)發(fā)生GC則清空) GC 運行后終止
虛引用 在垃圾回收時 聯(lián)合 ReferenceQueue 來跟蹤對象被垃圾回收器回收的活動 GC 運行后終止

在 Java/Android 開發(fā)中,為了防止內(nèi)存溢出,在處理一些占內(nèi)存大而且生命周期比較長對象的時候,可以盡量應(yīng)用軟引用和弱引用,軟/弱引用可以和一個引用隊列(ReferenceQueue)聯(lián)合使用,如果軟引用所引用的對象被垃圾回收器回收,Java 虛擬機就會把這個軟引用加入到與之關(guān)聯(lián)的引用隊列中,利用這個隊列可以得知被回收的軟/弱引用的對象列表,從而為緩沖器清除已失效的軟/弱引用。

Android 內(nèi)存泄漏和優(yōu)化

?具體的請看中篇:Android 性能優(yōu)化之內(nèi)存泄漏檢測以及內(nèi)存優(yōu)化(中)和下篇:Android 性能優(yōu)化之內(nèi)存泄漏檢測以及內(nèi)存優(yōu)化(下)

引用

http://blog.csdn.net/luoshengyang/article/details/42555483
http://blog.csdn.net/luoshengyang/article/details/41688319
http://blog.csdn.net/luoshengyang/article/details/42492621
http://blog.csdn.net/luoshengyang/article/details/41338251
http://blog.csdn.net/luoshengyang/article/details/41581063
https://mp.weixin.qq.com/s?__biz=MzA4MzEwOTkyMQ==&mid=2667377215&idx=1&sn=26e3e9ec5f4cf3e7ed1e90a0790cc071&chksm=84f32371b384aa67166a3ff60e3f8ffdfbeed17b4c8b46b538d5a3eec524c9d0bcac33951a1a&scene=0&key=c2240201df732cf062d22d3cf95164740442d817864520af90bb0e71fa51102f2e91475a4f597ec20653c59d305c8a3e518d3f575d419dfcf8fb63a776e0d9fa6d3a9a6a52e84fedf3f467fe4af1ba8b&ascene=0&uin=Mjg5MDI3NjQ2Mg%3D%3D&devicetype=iMac+MacBookPro11%2C4+OSX+OSX+10.12.3+build(16D32)&version=12010310&nettype=WIFI&fontScale=100&pass_ticket=Upl17Ws6QQsmZSia%2F%2B0xkZs9DYxAJBQicqh8rcaxYUjcu3ztlJUPxYrQKML%2BUtuf
http://geek.csdn.net/news/detail/127226
http://www.lxweimin.com/p/216b03c22bb8
https://zhuanlan.zhihu.com/p/25213586
https://joyrun.github.io/2016/08/08/AndroidMemoryLeak/
http://www.cnblogs.com/larack/p/6071209.html
https://source.android.com/devices/tech/dalvik/gc-debug.html
http://blog.csdn.net/high2011/article/details/53138202
http://gityuan.com/2015/10/03/Android-GC/
http://www.ayqy.net/blog/android-gc-log%E8%A7%A3%E8%AF%BB/
https://developer.android.com/studio/profile/investigate-ram.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,250評論 6 530
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 97,923評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,041評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,475評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,253評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,801評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,882評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,023評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,530評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,494評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,639評論 1 366
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,177評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 43,890評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,289評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,552評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,242評論 3 389
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,626評論 2 370

推薦閱讀更多精彩內(nèi)容