想寫一篇關(guān)于 android GC 的想法來源于追查一個魅族手機(jī)圖片滑動卡頓問題,由于不斷的 GC 導(dǎo)致的丟幀卡頓的問題讓我們想了很多方案去解決,所以就打算詳細(xì)的看看內(nèi)存分配和 GC 的原理,為什么會不斷的 GC,GC ALLOC 和 GC COCURRENT 有什么區(qū)別,能不能想辦法擴(kuò)大堆內(nèi)存減少 GC 的頻次等等。
從”GC Roots”集合開始,將內(nèi)存整個遍歷一次,保留所有可以被 GC Roots 直接或間接引用到的對象,而剩下的對象都當(dāng)作垃圾對待并回收,這個算法需要中斷進(jìn)程內(nèi)其它組件的執(zhí)行并且可能產(chǎn)生內(nèi)存碎片
將現(xiàn)有的內(nèi)存空間分為兩快,每次只使用其中一塊,在垃圾回收時將正在使用的內(nèi)存中的存活對象復(fù)制到未被使用的內(nèi)存塊中,之后,清除正在使用的內(nèi)存塊中的所有對象,交換兩個內(nèi)存的角色,完成垃圾回收。
先需要從根節(jié)點(diǎn)開始對所有可達(dá)對象做一次標(biāo)記,但之后,它并不簡單地清理未標(biāo)記的對象,而是將所有的存活對象壓縮到內(nèi)存的一端。之后,清理邊界外所有的空間。這種方法既避免了碎片的產(chǎn)生,又不需要兩塊相同的內(nèi)存空間,因此,其性價(jià)比比較高。
將所有的新建對象都放入稱為年輕代的內(nèi)存區(qū)域,年輕代的特點(diǎn)是對象會很快回收,因此,在年輕代就選擇效率較高的復(fù)制算法。當(dāng)一個對象經(jīng)過幾次回收后依然存活,對象就會被放入稱為老生代的內(nèi)存空間。對于新生代適用于復(fù)制算法,而對于老年代則采取標(biāo)記-壓縮算法。
乍一看這兩個算法似乎并沒有多大的區(qū)別,都是標(biāo)記了然后挪到另外的內(nèi)存地址進(jìn)行回收,那為什么不同的分代要使用不同的回收算法呢?
其實(shí) 2 者大的區(qū)別在于前者是用空間換時間后者則是用時間換空間。
前者的在工作的時候是不沒有獨(dú)立的“mark”與“copy”階段的,而是合在一起做一個動作,就叫 scavenge(或 evacuate,或者就叫 copy)。也就是說,每發(fā)現(xiàn)一個這次收集中尚未訪問過的活對象就直接 copy 到新地方,同時設(shè)置 forwarding pointer。這樣的工作方式就需要多一份空間。
后者在工作的時候則需要分別的 mark 與 compact 階段,mark 階段用來發(fā)現(xiàn)并標(biāo)記所有活的對象,然后 compact 階段才移動對象來達(dá)到 compact 的目的。如果 compact 方式是 sliding compaction,則在 mark 之后就可以按順序一個個對象“滑動”到空間的某一側(cè)。因?yàn)橐呀?jīng)先遍歷了整個空間里的對象圖,知道所有的活對象了,所以移動的時候就可以在同一個空間內(nèi)而不需要多一份空間。
所以新生代的回收會更快一點(diǎn),老年代的回收則會需要更長時間,同時壓縮階段是會暫停應(yīng)用的,所以給我們應(yīng)該盡量避免對象出現(xiàn)在老年代。
Java 堆實(shí)際上是由一個 Active 堆和一個 Zygote 堆組成的,其中,Zygote 堆用來管理 Zygote 進(jìn)程在啟動過程中預(yù)加載和創(chuàng)建的各種對象,而 Active 堆是在 Zygote 進(jìn)程 fork 個子進(jìn)程之前創(chuàng)建的。以后啟動的所有應(yīng)用程序進(jìn)程是被 Zygote 進(jìn)程 fork 出來的,并都持有一個自己的 Dalvik 虛擬機(jī)。在創(chuàng)建應(yīng)用程序的過程中,Dalvik 虛擬機(jī)采用 COW 策略復(fù)制 Zygote 進(jìn)程的地址空間。
COW 策略:一開始的時候(未復(fù)制 Zygote 進(jìn)程的地址空間的時候),應(yīng)用程序進(jìn)程和 Zygote 進(jìn)程共享了同一個用來分配對象的堆。當(dāng) Zygote 進(jìn)程或者應(yīng)用程序進(jìn)程對該堆進(jìn)行寫操作時,內(nèi)核就會執(zhí)行真正的拷貝操作,使得 Zygote 進(jìn)程和應(yīng)用程序進(jìn)程分別擁有自己的一份拷貝,這就是所謂的 COW。因?yàn)?copy 是十分耗時的,所以必須盡量避免 copy 或者盡量少的 copy。
為了實(shí)現(xiàn)這個目的,當(dāng)創(chuàng)建個應(yīng)用程序進(jìn)程時,會將已經(jīng)使用了的那部分堆內(nèi)存劃分為一部分,還沒有使用的堆內(nèi)存劃分為另外一部分。前者就稱為 Zygote 堆,后者就稱為 Active 堆。這樣只需把 zygote 堆中的內(nèi)容復(fù)制給應(yīng)用程序進(jìn)程就可以了。以后無論是 Zygote 進(jìn)程,還是應(yīng)用程序進(jìn)程,當(dāng)它們需要分配對象的時候,都在 Active 堆上進(jìn)行。這樣就可以使得 Zygote 堆盡可能少地被執(zhí)行寫操作,因而就可以減少執(zhí)行寫時拷貝的操作。在 Zygote 堆里面分配的對象其實(shí)主要就是 Zygote 進(jìn)程在啟動過程中預(yù)加載的類、資源和對象了。這意味著這些預(yù)加載的類、資源和對象可以在 Zygote 進(jìn)程和應(yīng)用程序進(jìn)程中做到長期共享。這樣既能減少拷貝操作,還能減少對內(nèi)存的需求。
記得我們之前在優(yōu)化魅族某手機(jī)的 gc 卡頓問題時,發(fā)現(xiàn)他很容易觸發(fā) GC_FOR_MALLOC,這個 GC 類別后續(xù)會說到,是分配對象內(nèi)存不足時導(dǎo)致的。可是我們又設(shè)置了很大的堆 Size 為什么還會內(nèi)存不夠呢,這里需要了解以下幾個概念:分別是 Java 堆的起始大小(Starting Size)、大值(Maximum Size)和增長上限值(Growth Limit)。
在啟動 Dalvik 虛擬機(jī)的時候,我們可以分別通過-Xms、-Xmx和-XX:HeapGrowthLimit三個選項(xiàng)來指定上述三個值,以上三個值分別表示表示
Starting Size: Dalvik 虛擬機(jī)啟動的時候,會先分配一塊初始的堆內(nèi)存給虛擬機(jī)使用。
Growth Limit:是系統(tǒng)給每一個程序的大堆上限,超過這個上限,程序就會 OOM
Maximum Size:不受控情況下的大堆內(nèi)存大小,起始就是我們在用 largeheap 屬性的時候,可以從系統(tǒng)獲取的大堆大小
同時除了上面的這個三個指標(biāo)外,還有幾個指標(biāo)也是值得我們關(guān)注的,那就是堆小空閑值(Min Free)、堆大空閑值(Max Free)和堆目標(biāo)利用率(Target Utilization)。假設(shè)在某一次 GC 之后,存活對象占用內(nèi)存的大小為 LiveSize,那么這時候堆的理想大小應(yīng)該為(LiveSize / U)。但是(LiveSize / U)必須大于等于(LiveSize + MinFree)并且小于等于(LiveSize + MaxFree),每次 GC 后垃圾回收器都會盡量讓堆的利用率往目標(biāo)利用率靠攏。所以當(dāng)我們嘗試手動去生成一些幾百 K 的對象,試圖去擴(kuò)大可用堆大小的時候,反而會導(dǎo)致頻繁的 GC,因?yàn)檫@些對象的分配會導(dǎo)致 GC,而 GC 后會讓堆內(nèi)存回到合適的比例,而我們使用的局部變量很快會被回收理論上存活對象還是那么多,我們的堆大小也會縮減回來無法達(dá)到擴(kuò)充的目的。 與此同時這也是產(chǎn)生 CONCURRENT GC 的一個因素,后文我們會詳細(xì)講到。
GC_FOR_MALLOC: 表示是在堆上分配對象時內(nèi)存不足觸發(fā)的 GC。
GC_CONCURRENT: 當(dāng)我們應(yīng)用程序的堆內(nèi)存達(dá)到一定量,或者可以理解為快要滿的時候,系統(tǒng)會自動觸發(fā) GC 操作來釋放內(nèi)存。
GC_EXPLICIT: 表示是應(yīng)用程序調(diào)用 System.gc、VMRuntime.gc 接口或者收到 SIGUSR1 信號時觸發(fā)的 GC。
GC_BEFORE_OOM: 表示是在準(zhǔn)備拋 OOM 異常之前進(jìn)行的后努力而觸發(fā)的 GC。
實(shí)際上,GC_FOR_MALLOC、GC_CONCURRENT 和 GC_BEFORE_OOM 三種類型的 GC 都是在分配對象的過程觸發(fā)的。而并發(fā)和非并發(fā) GC 的區(qū)別主要在于前者在 GC 過程中,有條件地掛起和喚醒非 GC 線程,而后者在執(zhí)行 GC 的過程中,一直都是掛起非 GC 線程的。并行 GC 通過有條件地掛起和喚醒非 GC 線程,就可以使得應(yīng)用程序獲得更好的響應(yīng)性。但是同時并行 GC 需要多執(zhí)行一次標(biāo)記根集對象以及遞歸標(biāo)記那些在 GC 過程被訪問了的對象的操作,所以也需要花費(fèi)更多的 CPU 資源。后文在 Art 的并發(fā)和非并發(fā) GC 中我們也會著重說明下這兩者的區(qū)別。
調(diào)用函數(shù) dvmHeapSourceAlloc 在 Java 堆上分配指定大小的內(nèi)存。如果分配成功,那么就將分配得到的地址直接返回給調(diào)用者了。函數(shù) dvmHeapSourceAlloc 在不改變 Java 堆當(dāng)前大小的前提下進(jìn)行內(nèi)存分配,這是屬于輕量級的內(nèi)存分配動作。
如果上一步內(nèi)存分配失敗,這時候就需要執(zhí)行一次 GC 了。不過如果 GC 線程已經(jīng)在運(yùn)行中,即 gDvm.gcHeap->gcRunning 的值等于 true,那么就直接調(diào)用函數(shù) dvmWaitForConcurrentGcToComplete 等到 GC 執(zhí)行完成就是了。否則的話,就需要調(diào)用函數(shù) gcForMalloc 來執(zhí)行一次 GC 了,參數(shù) false 表示不要回收軟引用對象引用的對象。
GC 執(zhí)行完畢后,再次調(diào)用函數(shù) dvmHeapSourceAlloc 嘗試輕量級的內(nèi)存分配操作。如果分配成功,那么就將分配得到的地址直接返回給調(diào)用者了。
如果上一步內(nèi)存分配失敗,這時候就得考慮先將 Java 堆的當(dāng)前大小設(shè)置為 Dalvik 虛擬機(jī)啟動時指定的 Java 堆大值,再進(jìn)行內(nèi)存分配了。這是通過調(diào)用函數(shù) dvmHeapSourceAllocAndGrow 來實(shí)現(xiàn)的。
如果調(diào)用函數(shù) dvmHeapSourceAllocAndGrow 分配內(nèi)存成功,則直接將分配得到的地址直接返回給調(diào)用者了。
如果上一步內(nèi)存分配還是失敗,這時候就得出狠招了。再次調(diào)用函數(shù) gcForMalloc 來執(zhí)行 GC。參數(shù) true 表示要回收軟引用對象引用的對象。
GC 執(zhí)行完畢,再次調(diào)用函數(shù) dvmHeapSourceAllocAndGrow 進(jìn)行內(nèi)存分配。這是后一次努力了,成功與事都到此為止。
示例圖如下:
通過這個流程可以看到,在對象的分配中會導(dǎo)致 GC,次分配對象失敗我們會觸發(fā) GC 但是不回收 Soft 的引用,如果再次分配還是失敗我們就會將 Soft 的內(nèi)存也給回收,前者觸發(fā)的 GC 是 GC_FOR_MALLOC 類型的 GC,后者是 GC_BEFORE_OOM 類型的 GC。而當(dāng)內(nèi)存分配成功后,我們會判斷當(dāng)前的內(nèi)存占用是否是達(dá)到了 GC_CONCURRENT 的閥值,如果達(dá)到了那么又會觸發(fā) GC_CONCURRENT。
那么這個閥值又是如何來的呢,上面我們說到的一個目標(biāo)利用率,GC 后我們會記錄一個目標(biāo)值,這個值理論上需要再上述的范圍之內(nèi),如果不在我們會選取邊界值做為目標(biāo)值。虛擬機(jī)會記錄這個目標(biāo)值,當(dāng)做當(dāng)前允許總的可以分配到的內(nèi)存。同時根據(jù)目標(biāo)值減去固定值(200~500K),當(dāng)做觸發(fā) GC_CONCURRENT 事件的閾值。
主流的大部分 Davik 采取的都是標(biāo)注與清理(Mark and Sweep)回收算法,也有實(shí)現(xiàn)了拷貝 GC 的,這一點(diǎn)和 HotSpot 是不一樣的,具體使用什么算法是在編譯期決定的,無法在運(yùn)行的時候動態(tài)更換。如果在編譯 dalvik 虛擬機(jī)的命令中指明了”WITH_COPYING_GC”選項(xiàng),則編譯”/dalvik/vm/alloc/Copying.cpp”源碼 – 此是 Android 中拷貝 GC 算法的實(shí)現(xiàn),否則編譯”/dalvik/vm/alloc/HeapSource.cpp” – 其實(shí)現(xiàn)了標(biāo)注與清理 GC 算法。
由于 Mark and Sweep 算法的缺點(diǎn),容易導(dǎo)致內(nèi)存碎片,所以在這個算法下,當(dāng)我們有大量不連續(xù)小內(nèi)存的時候,再分配一個較大對象時,還是會非常容易導(dǎo)致 GC,比如我們在該手機(jī)上 decode 圖片,具體情況如下:
所以對于 Dalvik 虛擬機(jī)的手機(jī)來說,我們首先要盡量避免掉頻繁生成很多臨時小變量(比如說:getView,onDraw 等函數(shù)),另一個又要盡量去避免產(chǎn)生很多長生命周期的大對象。
ART 運(yùn)行時內(nèi)部使用的 Java 堆的主要組成包括 Image Space、Zygote Space、Allocation Space 和 Large Object Space 四個 Space,Image Space 用來存在一些預(yù)加載的類, Zygote Space 和 Allocation Space 與 Dalvik 虛擬機(jī)垃圾收集機(jī)制中的 Zygote 堆和 Active 堆的作用是一樣的,
Large Object Space 就是一些離散地址的集合,用來分配一些大對象從而提高了 GC 的管理效率和整體性能,類似如下圖:
在下文的 GC Log 中,我們也能看到在 art 的 GC Log 中包含了 LOS 的信息,方便我們查看大內(nèi)存的情況。
kGcCauseForAlloc ,當(dāng)要分配內(nèi)存的時候發(fā)現(xiàn)內(nèi)存不夠的情況下引起的 GC,這種情況下的 GC 會 stop world
kGcCauseBackground,當(dāng)內(nèi)存達(dá)到一定的閥值的時候會去出發(fā) GC,這個時候是一個后臺 gc,不會引起 stop world
kGcCauseExplicit,顯示調(diào)用的時候進(jìn)行的 gc,如果 art 打開了這個選項(xiàng)的情況下,在 system.gc 的時候會進(jìn)行 gc
其他更多
由于 Art 下內(nèi)存分配和 Dalvik 下基本沒有任何區(qū)別,我直接貼圖帶過了。
Art 在 GC 上不像 Dalvik 僅有一種回收算法,Art 在不同的情況下會選擇不同的回收算法,比如 Alloc 內(nèi)存不夠的時候會采用非并發(fā) GC,而在 Alloc 后發(fā)現(xiàn)內(nèi)存達(dá)到一定閥值的時候又會觸發(fā)并發(fā) GC。同時在前后臺的情況下 GC 策略也不盡相同,后面我們會一一給大家說明。
步驟 1. 調(diào)用子類實(shí)現(xiàn)的成員函數(shù) InitializePhase 執(zhí)行 GC 初始化階段。
步驟 2. 掛起所有的 ART 運(yùn)行時線程。
步驟 3. 調(diào)用子類實(shí)現(xiàn)的成員函數(shù) MarkingPhase 執(zhí)行 GC 標(biāo)記階段。
步驟 4. 調(diào)用子類實(shí)現(xiàn)的成員函數(shù) ReclaimPhase 執(zhí)行 GC 回收階段。
步驟 5. 恢復(fù)第 2 步掛起的 ART 運(yùn)行時線程。
步驟 6. 調(diào)用子類實(shí)現(xiàn)的成員函數(shù) FinishPhase 執(zhí)行 GC 結(jié)束階段。
步驟 1. 調(diào)用子類實(shí)現(xiàn)的成員函數(shù) InitializePhase 執(zhí)行 GC 初始化階段。
步驟 2. 獲取用于訪問 Java 堆的鎖。
步驟 3. 調(diào)用子類實(shí)現(xiàn)的成員函數(shù) MarkingPhase 執(zhí)行 GC 并行標(biāo)記階段。
步驟 4. 釋放用于訪問 Java 堆的鎖。
步驟 5. 掛起所有的 ART 運(yùn)行時線程。
步驟 6. 調(diào)用子類實(shí)現(xiàn)的成員函數(shù) HandleDirtyObjectsPhase 處理在 GC 并行標(biāo)記階段被修改的對象。。
步驟 7. 恢復(fù)第 4 步掛起的 ART 運(yùn)行時線程。
步驟 8. 重復(fù)第 5 到第 7 步,直到所有在 GC 并行階段被修改的對象都處理完成。
步驟 9. 獲取用于訪問 Java 堆的鎖。
步驟 10. 調(diào)用子類實(shí)現(xiàn)的成員函數(shù) ReclaimPhase 執(zhí)行 GC 回收階段。
步驟 11. 釋放用于訪問 Java 堆的鎖。
步驟 12. 調(diào)用子類實(shí)現(xiàn)的成員函數(shù) FinishPhase 執(zhí)行 GC 結(jié)束階段。
所以不論是并發(fā)還是非并發(fā),都會引起 stopworld 的情況出現(xiàn),并發(fā)的情況下單次 stopworld 的時間會更短,基本區(qū)別和。
首先可以通過如下 2 張圖來對比下
Dalvik GC:
Art GC:
Art 的并發(fā) GC 和 Dalvik 的并發(fā) GC 有什么區(qū)別呢,初看好像 2 者差不多,雖然沒有一直掛起線程,但是也會有暫停線程去執(zhí)行標(biāo)記對象的流程。通過閱讀相關(guān)文檔可以了解到 Art 并發(fā) GC 對于 Dalvik 來說主要有三個優(yōu)勢點(diǎn):
Art 在對象分配時會將新分配的對象壓入到 Heap 類的成員變量 allocation_stack_ 描述的 Allocation Stack 中去,從而可以一定程度縮減對象遍歷范圍。
對于標(biāo)記 Allocation Stack 的內(nèi)存時,會預(yù)讀取接下來要遍歷的對象,同時再取出來該對象后又會將該對象引用的其他對象壓入棧中,直至遍歷完畢。
在 Mark 階段是不會 Block 其他線程的,這個階段會有臟數(shù)據(jù),比如 Mark 發(fā)現(xiàn)不會使用的但是這個時候又被其他線程使用的數(shù)據(jù),在 Mark 階段也會處理一些臟數(shù)據(jù)而不是留在后 Block 的時候再去處理,這樣也會減少后面 Block 階段對于臟數(shù)據(jù)的處理的時間。
前臺 Foreground 指的就是應(yīng)用程序在前臺運(yùn)行時,而后臺 Background 就是應(yīng)用程序在后臺運(yùn)行時。因此,F(xiàn)oreground GC 就是應(yīng)用程序在前臺運(yùn)行時執(zhí)行的 GC,而 Background 就是應(yīng)用程序在后臺運(yùn)行時執(zhí)行的 GC。
應(yīng)用程序在前臺運(yùn)行時,響應(yīng)性是重要的,因此也要求執(zhí)行的 GC 是高效的。相反,應(yīng)用程序在后臺運(yùn)行時,響應(yīng)性不是重要的,這時候就適合用來解決堆的內(nèi)存碎片問題。因此,Mark-Sweep GC 適合作為 Foreground GC,而 Mark-Compact GC 適合作為 Background GC。
由于有 Compact 的能力存在,碎片化在 Art 上可以很好的被避免,這個也是 Art 一個很好的能力。
總的來看,art 在 gc 上做的比 dalvik 好太多了,不光是 gc 的效率,減少 pause 時間,而且還在內(nèi)存分配上對大內(nèi)存的有單獨(dú)的分配區(qū)域,同時還能有算法在后臺做內(nèi)存整理,減少內(nèi)存碎片。對于開發(fā)者來說 art 下我們基本可以避免很多類似 gc 導(dǎo)致的卡頓問題了。另外根據(jù)谷歌自己的數(shù)據(jù)來看,Art 相對 Dalvik 內(nèi)存分配的效率提高了 10 倍,GC 的效率提高了 2-3 倍。
當(dāng)我們想要根據(jù) GC 日志來追查一些 GC 可能造成的卡頓時,我們需要了解 GC 日志的組成,不同信息代表了什么含義。
dalvik 的日志格式基本如下:
D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>, <Pause_time>, <Total_time>
gc_reason:就是我們上文提到的,是 gc_alloc 還是 gc_concurrent,了解到不同的原因方便我們做不同的處理。
amount_freed:表示系統(tǒng)通過這次 GC 操作釋放了多少內(nèi)存
Heap_stats:中會顯示當(dāng)前內(nèi)存的空閑比例以及使用情況(活動對象所占內(nèi)存 / 當(dāng)前程序總內(nèi)存)
Pause_time:表示這次 GC 操作導(dǎo)致應(yīng)用程序暫停的時間。關(guān)于這個暫停的時間,在 2.3 之前 GC 操作是不能并發(fā)進(jìn)行的,也就是系統(tǒng)正在進(jìn)行 GC,那么應(yīng)用程序就只能阻塞住等待 GC 結(jié)束。而自 2.3 之后,GC 操作改成了并發(fā)的方式進(jìn)行,就是說 GC 的過程中不會影響到應(yīng)用程序的正常運(yùn)行,但是在 GC 操作的開始和結(jié)束的時候會短暫阻塞一段時間,所以還有后續(xù)的一個 total_time。
Total_time:表示本次 GC 所花費(fèi)的總時間和上面的 Pause_time,也就是 stop all 是不一樣的,卡頓時間主要看上面的 pause_time。
I/art: <GC_Reason> <Amount_freed>, <LOS_Space_Status>, <Heap_stats>, <Pause_time>, <Total_time>
基本情況和 Dalvik 沒有什么差別,GC 的 Reason 更多了,還多了一個 OS_Space_Status
本站文章版權(quán)歸原作者及原出處所有 。內(nèi)容為作者個人觀點(diǎn), 并不代表本站贊同其觀點(diǎn)和對其真實(shí)性負(fù)責(zé),本站只提供參考并不構(gòu)成任何投資及應(yīng)用建議。本站是一個個人學(xué)習(xí)交流的平臺,網(wǎng)站上部分文章為轉(zhuǎn)載,并不用于任何商業(yè)目的,我們已經(jīng)盡可能的對作者和來源進(jìn)行了通告,但是能力有限或疏忽,造成漏登,請及時聯(lián)系我們,我們將根據(jù)著作權(quán)人的要求,立即更正或者刪除有關(guān)內(nèi)容。本站擁有對此聲明的最終解釋權(quán)。