共計 7852 個字符,預計需要花費 20 分鐘才能閱讀完成。
這篇文章主要介紹了 Android GC 的知識點有哪些的相關知識,內容詳細易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇 Android GC 的知識點有哪些文章都會有所收獲,下面我們一起來看看吧。
1. JVM 內存回收機制 1.1. 回收算法
標記回收算法(Mark and Sweep GC)
從 GC Roots 集合開始,將內存整個遍歷一次,保留所有可以被 GC Roots 直接或間接引用到的對象,而剩下的對象都當作垃圾對待并回收,這個算法需要中斷進程內其它組件的執行并且可能產生內存碎片。
復制算法 (Copying)
將現有的內存空間分為兩快,每次只使用其中一塊,在垃圾回收時將正在使用的內存中的存活對象復制到未被使用的內存塊中,之后,清除正在使用的內存塊中的所有對象,交換兩個內存的角色,完成垃圾回收。
標記 - 壓縮算法 (Mark-Compact)
先需要從根節點開始對所有可達對象做一次標記,但之后,它并不簡單地清理未標記的對象,而是將所有的存活對象壓縮到內存的一端。之后,清理邊界外所有的空間。這種方法既避免了碎片的產生,又不需要兩塊相同的內存空間,因此,其性價比比較高。
分代
將所有的新建對象都放入稱為年輕代的內存區域,年輕代的特點是對象會很快回收,因此,在年輕代就選擇效率較高的復制算法。當一個對象經過幾次回收后依然存活,對象就會被放入稱為老生代的內存空間。對于新生代適用于復制算法,而對于老年代則采取標記 - 壓縮算法。
1.2. 復制和標記 - 壓縮算法的區別
乍一看這兩個算法似乎并沒有多大的區別,都是標記了然后挪到另外的內存地址進行回收,那為什么不同的分代要使用不同的回收算法呢?
其實 2 者最大的區別在于前者是用空間換時間后者則是用時間換空間。
前者的在工作的時候是不沒有獨立的“Mark”與“Copy”階段的,而是合在一起做一個動作,就叫 Scavenge(或 Evacuate,或者就叫 Copy)。也就是說,每發現一個這次收集中尚未訪問過的活對象就直接 Copy 到新地方,同時設置 Forwarding Pointer,這樣的工作方式就需要多一份空間。
后者在工作的時候則需要分別的 Mark 與 Compact 階段,Mark 階段用來發現并標記所有活的對象,然后 compact 階段才移動對象來達到 Compact 的目的。如果 Compact 方式是 Sliding Compaction,則在 Mark 之后就可以按順序一個個對象“滑動”到空間的某一側。因為已經先遍歷了整個空間里的對象圖,知道所有的活對象了,所以移動的時候就可以在同一個空間內而不需要多一份空間。
所以新生代的回收會更快一點,老年代的回收則會需要更長時間,同時壓縮階段是會暫停應用的,所以給我們應該盡量避免對象出現在老年代。
2. Dalvik 虛擬機 2.1. Java 堆
Java 堆實際上是由一個 Active 堆和一個 Zygote 堆組成的,其中,Zygote 堆用來管理 Zygote 進程在啟動過程中預加載和創建的各種對象,而 Active 堆是在 Zygote 進程 Fork 第一個子進程之前創建的。以后啟動的所有應用程序進程是被 Zygote 進程 Fork 出來的,并都持有一個自己的 Dalvik 虛擬機。在創建應用程序的過程中,Dalvik 虛擬機采用 Cow 策略復制 Zygote 進程的地址空間。
Cow 策略:一開始的時候(未復制 Zygote 進程的地址空間的時候),應用程序進程和 Zygote 進程共享了同一個用來分配對象的堆。當 Zygote 進程或者應用程序進程對該堆進行寫操作時,內核就會執
行真正的拷貝操作,使得 Zygote 進程和應用程序進程分別擁有自己的一份拷貝,這就是所謂的 Cow。因為 Copy 是十分耗時的,所以必須盡量避免 Copy 或者盡量少的 Copy。
為了實現這個目的,當創建第一個應用程序進程時,會將已經使用了的那部分堆內存劃分為一部分,還沒有使用的堆內存劃分為另外一部分。前者就稱為 Zygote 堆,后者就稱為 Active 堆。這樣只需把 zygote 堆中的內容復制給應用程序進程就可以了。以后無論是 Zygote 進程,還是應用程序進程,當它們需要分配對象的時候,都在 Active 堆上進行。這樣就可以使得 Zygote 堆盡可能少地被執行寫操作,因而就可以減少執行寫時拷貝的操作。在 Zygote 堆里面分配的對象其實主要就是 Zygote 進程在啟動過程中預加載的類、資源和對象了。這意味著這些預加載的類、資源和對象可以在 Zygote 進程和應用程序進程中做到長期共享。這樣既能減少拷貝操作,還能減少對內存的需求。
2.2. 和 GC 有關的一些指標
記得我們之前在優化魅族某手機的 gc 卡頓問題時,發現他很容易觸發 GC_FOR_MALLOC,這個 GC 類別后續會說到,是分配對象內存不足時導致的。可是我們又設置了很大的堆 Size 為什么還會內存不夠呢,這里需要了解以下幾個概念:分別是 Java 堆的起始大小(Starting Size)、最大值(Maximum Size)和增長上限值(Growth Limit)。
在啟動 Dalvik 虛擬機的時候,我們可以分別通過 -Xms、-Xmx 和 -XX:HeapGrowthLimit 三個選項來指定上述三個值,以上三個值分別表示表示:
Starting Size: Dalvik 虛擬機啟動的時候,會先分配一塊初始的堆內存給虛擬機使用。
Growth Limit: 是系統給每一個程序的最大堆上限, 超過這個上限,程序就會 OOM。
Maximum Size: 不受控情況下的最大堆內存大小,起始就是我們在用 largeheap 屬性的時候,可以從系統獲取的最大堆大小。
同時除了上面的這個三個指標外,還有幾個指標也是值得我們關注的,那就是堆最小空閑值(Min Free)、堆最大空閑值(Max Free)和堆目標利用率(Target Utilization)。假設在某一次 GC 之后,存活對象占用內存的大小為 LiveSize,那么這時候堆的理想大小應該為 (LiveSize / U)。但是(LiveSize / U) 必須大于等于 (LiveSize + MinFree) 并且小于等于(LiveSize + MaxFree),每次 GC 后垃圾回收器都會盡量讓堆的利用率往目標利用率靠攏。所以當我們嘗試手動去生成一些幾百 K 的對象,試圖去擴大可用堆大小的時候,反而會導致頻繁的 GC,因為這些對象的分配會導致 GC,而 GC 后會讓堆內存回到合適的比例,而我們使用的局部變量很快會被回收理論上存活對象還是那么多,我們的堆大小也會縮減回來無法達到擴充的目的。與此同時這也是產生 CONCURRENT GC 的一個因素,后文我們會詳細講到。
2.3. GC 的類型
GC_FOR_MALLOC: 表示是在堆上分配對象時內存不足觸發的 GC。
GC_CONCURRENT: 當我們應用程序的堆內存達到一定量,或者可以理解為快要滿的時候,系統會自動觸發 GC 操作來釋放內存。
GC_EXPLICIT: 表示是應用程序調用 System.gc、VMRuntime.gc 接口或者收到 SIGUSR1 信號時觸發的 GC。
GC_BEFORE_OOM: 表示是在準備拋 OOM 異常之前進行的最后努力而觸發的 GC。
實際上,GC_FOR_MALLOC、GC_CONCURRENT 和 GC_BEFORE_OOM 三種類型的 GC 都是在分配對象的過程觸發的。而并發和非并發 GC 的區別主要在于前者在 GC 過程中,有條件地掛起和喚醒非 GC 線程,而后者在執行 GC 的過程中,一直都是掛起非 GC 線程的。并行 GC 通過有條件地掛起和喚醒非 GC 線程,就可以使得應用程序獲得更好的響應性。但是同時并行 GC 需要多執行一次標記根集對象以及遞歸標記那些在 GC 過程被訪問了的對象的操作,所以也需要花費更多的 CPU 資源。后文在 ART 的并發和非并發 GC 中我們也會著重說明下這兩者的區別。
2.4. 對象的分配和 GC 觸發時機
調用函數 dvmHeapSourceAlloc 在 Java 堆上分配指定大小的內存。如果分配成功,那么就將分配得到的地址直接返回給調用者了。函數 dvmHeapSourceAlloc 在不改變 Java 堆當前大小的前提下進行內存分配,這是屬于輕量級的內存分配動作。
如果上一步內存分配失敗,這時候就需要執行一次 GC 了。不過如果 GC 線程已經在運行中,即 gDvm.gcHeap- gcRunning 的值等于 true,那么就直接調用函數 dvmWaitForConcurrentGcToComplete 等到 GC 執行完成就是了。否則的話,就需要調用函數 gcForMalloc 來執行一次 GC 了,參數 false 表示不要回收軟引用對象引用的對象。
GC 執行完畢后,再次調用函數 dvmHeapSourceAlloc 嘗試輕量級的內存分配操作。如果分配成功,那么就將分配得到的地址直接返回給調用者了。
如果上一步內存分配失敗,這時候就得考慮先將 Java 堆的當前大小設置為 Dalvik 虛擬機啟動時指定的 Java 堆最大值,再進行內存分配了。這是通過調用函數 dvmHeapSourceAllocAndGrow 來實現的。
如果調用函數 dvmHeapSourceAllocAndGrow 分配內存成功,則直接將分配得到的地址直接返回給調用者了。
如果上一步內存分配還是失敗,這時候就得出狠招了。再次調用函數 gcForMalloc 來執行 GC。參數 true 表示要回收軟引用對象引用的對象。
GC 執行完畢,再次調用函數 dvmHeapSourceAllocAndGrow 進行內存分配。這是最后一次努力了,成功與事都到此為止。
示例圖如下:
通過這個流程可以看到,在對象的分配中會導致 GC,第一次分配對象失敗我們會觸發 GC 但是不回收 Soft 的引用,如果再次分配還是失敗我們就會將 Soft 的內存也給回收,前者觸發的 GC 是 GC_FOR_MALLOC 類型的 GC,后者是 GC_BEFORE_OOM 類型的 GC。而當內存分配成功后,我們會判斷當前的內存占用是否是達到了 GC_CONCURRENT 的閥值,如果達到了那么又會觸發 GC_CONCURRENT。
那么這個閥值又是如何來的呢,上面我們說到的一個目標利用率,GC 后我們會記錄一個目標值,這個值理論上需要再上述的范圍之內,如果不在我們會選取邊界值做為目標值。虛擬機會記錄這個目標值,當做當前允許總的可以分配到的內存。同時根據目標值減去固定值(200~500K), 當做觸發 GC_CONCURRENT 事件的閾值。
2.5. 回收算法和內存碎片
主流的大部分 Davik 采取的都是標注與清理(Mark and Sweep)回收算法,也有實現了拷貝 GC 的,這一點和 HotSpot 是不一樣的,具體使用什么算法是在編譯期決定的,無法在運行的時候動態更換。如果在編譯 dalvik 虛擬機的命令中指明了 WITH_COPYING_GC 選項,則編譯 /dalvik/vm/alloc/Copying.cpp 源碼 – 此是 Android 中拷貝 GC 算法的實現,否則編譯 /dalvik/vm/alloc/HeapSource.cpp – 其實現了標注與清理 GC 算法。
由于 Mark and Sweep 算法的缺點,容易導致內存碎片,所以在這個算法下,當我們有大量不連續小內存的時候,再分配一個較大對象時,還是會非常容易導致 GC,比如我們在該手機上 decode 圖片,具體情況如下:
所以對于 Dalvik 虛擬機的手機來說,我們首先要盡量避免掉頻繁生成很多臨時小變量(比如說:getView, onDraw 等函數中 new 對象),另一個又要盡量去避免產生很多長生命周期的大對象。
3. ART 內存回收機制 3.1. Java 堆
ART 運行時內部使用的 Java 堆的主要組成包括 Image Space、Zygote Space、Allocation Space 和 Large Object Space 四個 Space,Image Space 用來存在一些預加載的類,Zygote Space 和 Allocation Space 與 Dalvik 虛擬機垃圾收集機制中的 Zygote 堆和 Active 堆的作用是一樣的,
Large Object Space 就是一些離散地址的集合,用來分配一些大對象從而提高了 GC 的管理效率和整體性能,類似如下圖:
在下文的 GC Log 中,我們也能看到在 ART 的 GC Log 中包含了 LOS 的信息,方便我們查看大內存的情況。
3.2. GC 的類型
kGcCauseForAlloc: 當要分配內存的時候發現內存不夠的情況下引起的 GC,這種情況下的 GC 會 Stop World.
kGcCauseBackground: 當內存達到一定的閥值的時候會去出發 GC,這個時候是一個后臺 GC,不會引起 Stop World.
kGcCauseExplicit,顯示調用的時候進行的 gc,如果 ART 打開了這個選項的情況下,在 system.gc 的時候會進行 GC.
其他更多。
3.3. 對象的分配和 GC 觸發時機
由于 ART 下內存分配和 Dalvik 下基本沒有任何區別,我直接貼圖帶過了。
3.4. 并發和非并發 GC
ART 在 GC 上不像 Dalvik 僅有一種回收算法,ART 在不同的情況下會選擇不同的回收算法,比如 Alloc 內存不夠的時候會采用非并發 GC,而在 Alloc 后發現內存達到一定閥值的時候又會觸發并發 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. 恢復第 4 步掛起的 ART 運行時線程。
步驟 8. 重復第 5 到第 7 步,直到所有在 GC 并行階段被修改的對象都處理完成。
步驟 9. 獲取用于訪問 Java 堆的鎖。
步驟 10. 調用子類實現的成員函數 ReclaimPhase 執行 GC 回收階段。
步驟 11. 釋放用于訪問 Java 堆的鎖。
步驟 12. 調用子類實現的成員函數 FinishPhase 執行 GC 結束階段。
所以不論是并發還是非并發,都會引起 Stop World 的情況出現,并發的情況下單次 Stop World 的時間會更短,基本區別和 Dalvik 類似。
3.5. ART 并發和 Dalvik 并發 GC 的差異
首先可以通過如下 2 張圖來對比下。
Dalvik GC:
ART GC:
ART 的并發 GC 和 Dalvik 的并發 GC 有什么區別呢,初看好像 2 者差不多,雖然沒有一直掛起線程,但是也會有暫停線程去執行標記對象的流程。通過閱讀相關文檔可以了解到 ART 并發 GC 對于 Dalvik 來說主要有三個優勢點:
標記自身
ART 在對象分配時會將新分配的對象壓入到 Heap 類的成員變量 allocationstack 描述的 Allocation Stack 中去,從而可以一定程度上縮減對象遍歷范圍。
預讀取
對于標記 Allocation Stack 的內存時,會預讀取接下來要遍歷的對象,同時再取出來該對象后又會將該對象引用的其他對象壓入棧中,直至遍歷完畢。
減少 Suspend 時間
在 Mark 階段是不會 Block 其他線程的,這個階段會有臟數據,比如 Mark 發現不會使用的但是這個時候又被其他線程使用的數據,在 Mark 階段也會處理一些臟數據而不是留在最后 Block 的時候再去處理,這樣也會減少后面 Block 階段對于臟數據的處理的時間。
3.6. 前后臺 GC
前臺 Foreground 指的就是應用程序在前臺運行時,而后臺 Background 就是應用程序在后臺運行時。因此,Foreground GC 就是應用程序在前臺運行時執行的 GC,而 Background 就是應用程序在后臺運行時執行的 GC。
應用程序在前臺運行時,響應性是最重要的,因此也要求執行的 GC 是高效的。相反,應用程序在后臺運行時,響應性不是最重要的,這時候就適合用來解決堆的內存碎片問題。因此,Mark-Sweep GC 適合作為 Foreground GC,而 Mark-Compact GC 適合作為 Background GC。
由于有 Compact 的能力存在,碎片化在 ART 上可以很好的被避免,這個也是 ART 一個很好的能力。
3.7. ART 大法好
總的來看,ART 在 GC 上做的比 Dalvik 好太多了,不光是 GC 的效率,減少 Pause 時間,而且還在內存分配上對大內存的有單獨的分配區域,同時還能有算法在后臺做內存整理,減少內存碎片。對于開發者來說 ART 下我們基本可以避免很多類似 GC 導致的卡頓問題了。另外根據谷歌自己的數據來看,ART 相對 Dalvik 內存分配的效率提高了 10 倍,GC 的效率提高了 2 - 3 倍。
4. GC Log
當我們想要根據 GC 日志來追查一些 GC 可能造成的卡頓時,我們需要了解 GC 日志的組成,不同信息代表了什么含義。
4.1. Dalvik GC 日志
Dalvik 的日志格式基本如下:
D/dalvikvm: GC_Reason Amount_freed , Heap_stats , Pause_time , Total_time
GC_Reason: 就是我們上文提到的,是 gc_alloc 還是 gc_concurrent,了解到不同的原因方便我們做不同的處理。
Amount_freed: 表示系統通過這次 GC 操作釋放了多少內存。
Heap_stats: 中會顯示當前內存的空閑比例以及使用情況(活動對象所占內存 / 當前程序總內存)。
Pause_time: 表示這次 GC 操作導致應用程序暫停的時間。關于這個暫停的時間,在 2.3 之前 GC 操作是不能并發進行的,也就是系統正在進行 GC,那么應用程序就只能阻塞住等待 GC 結束。而自 2.3 之后,GC 操作改成了并發的方式進行,就是說 GC 的過程中不會影響到應用程序的正常運行,但是在 GC 操作的開始和結束的時候會短暫阻塞一段時間,所以還有后續的一個 total_time。
Total_time: 表示本次 GC 所花費的總時間和上面的 Pause_time, 也就是 stop all 是不一樣的,卡頓時間主要看上面的 pause_time。
4.2. ART GC 日志
I/art: GC_Reason Amount_freed , LOS_Space_Status , Heap_stats , Pause_time , Total_time
基本情況和 Dalvik 沒有什么差別,GC 的 Reason 更多了,還多了一個 OS_Space_Status.
LOS_Space_Status:Large Object Space,大對象占用的空間,這部分內存并不是分配在堆上的,但仍屬于應用程序內存空間,主要用來管理 bitmap 等占內存大的對象,避免因分配大內存導致堆頻繁 GC。
關于“Android GC 的知識點有哪些”這篇文章的內容就介紹到這里,感謝各位的閱讀!相信大家對“Android GC 的知識點有哪些”知識都有一定的了解,大家如果還想學習更多知識,歡迎關注丸趣 TV 行業資訊頻道。