前言
Android系統的APP運行需要依賴ART虛擬機(Android Runtime),ART虛擬機的主要作用是給APP的java代碼提供運行環境,其中編譯、執行、垃圾回收(GC)模塊是ART虛擬機的重中之重,GC使得java開發人員能專注于業務實作,而不用擔心記憶體泄漏,
此文章將簡要的向大家介紹ART虛擬機中Heap布局、常見GC型別和對應的問題案例,為大家分析優化應用提供一些思路,
本文基于的代碼和除錯手機系統為Android R(11)版本,
一、GC的相關配置
1. 記憶體回收器(回收演算法)、記憶體分配器
因為Android R支持讀屏障(kUseReadBarrier),在虛擬機創建階段,記憶體回收器設定為:
*前臺回收器foreground_collector_type = kCollectorTypeCC
*后臺回收器background_collector_type = kCollectorTypeCCBackground
##對于CC(ConcurrentCopying)并發復制回收器而言,前后臺回收器只用于配置前后臺GC時的一些回收器引數,實質回收器只有一種即kCollectorTypeCC,一般當應用退到后臺,系統希望應用盡可能釋放UI相關的垃圾物件并最大程度的進行記憶體整理,

##讀屏障(kUseReadBarrier)有三種實作方式,涉及到記憶體回收演算法的底層實作邏輯,讀者可自行了解,本文不做闡述,
ART虛擬機創建時先確定記憶體回收器的型別,進而系結對應的記憶體分配器,因回收器已設定為CC(ConcurrentCopying)即并發復制回收器,則Heap記憶體的主要分配區域定為RegionSpace,RegionSpace由一個個256KB的Region組成,對應的RegionSpace記憶體分配器定為kAllocatorTypeRegion,
從下圖代碼中可知,并發復制回收若啟用分代GC,默認回收策略是Sticky即只回收上次GC后新生成的物件,若未啟用分代,默認回收策略是Full即掃描所有space包括zygoteSpace的物件做回收,Android R版已啟用分代,則默認策略模式是Sticky,

二、ART虛擬機Heap記憶體布局
1. 應用Heap布局示例
作者使用Android R版本手機,安裝HelloWorld應用查看行程map檔案,應用的Heap布局如下圖,
此手機Heap引數和應用large_heap配置如下,應用可用堆上限為256MB,
[dalvik.vm.heapgrowthlimit]: [256m]
[dalvik.vm.heapsize]: [512m]
android:largeHeap="false"

2. Space型別
(1)RegionSpace [512M]:
A) 虛擬地址起點為300MB(0x12c00000),可看到圖中和ImageSpace之間地址不連續,
B) 虛擬地址范圍:為支持堆完整復制,需要額外預留一倍空間,當行程啟動后BindApplication階段呼叫ClampGrowthLimit()將RegionSpace 虛擬地址空間size設定為兩倍的堆上限,
C) 記憶體分配:絕大多數的物件分配的區域,RegionSpace由256KB大小的Region組成,Region的分配演算法是最簡單的指標碰撞(BumpPointer),
當一次分配請求到來,通過For回圈遍歷可用的Region,當已分配的Region數量已超過總數的一半,不允許再占用新的Region,只能依賴記憶體回收和整理來釋放記憶體用于分配,
D) 回收策略:當一個Region中存活物件占用大小超過75%時,此Region標記為kRegionTypeUnevacFromSpace,表示無需進行拷貝,否則將此Region中存活物件拷貝至標記成ToSpace的另一個Region中,拷貝完成后回收整個Region,立刻釋放256KB記憶體,
具體的演算法實作更加復雜,讀者可進一步了解CC演算法的實作,
(2) ImageSpace [4M]:
A) Zygote行程創建時,根據boot.art檔案ImageHeader結構體中指定的映射地址(0x70e44000),創建ImageSpace,將boot.art記憶體鏡像檔案映射到行程的虛擬地址空間,
B) ImageSpace不支持分配物件,也無需回收,
C) ImageSpace創建后同步映射boot.oat檔案到ImageSpace地址后面,
D) ImageSpace中映射的.art檔案可能有多個,比如boot-core-libart.art/boot-okhttp.art,boot.oat區域亦然,
(3)ZygoteSpace [3M]:
A)包含Zygote行程從創建到第一次Fork前所有存活的物件,當Fork新行程時無需再單獨映射或創建必需的物件,直接復用Zygote行程的資料,加快行程創建,
B)創建程序:
1 Zygote行程創建Heap時,申請一塊DlmallocSpace型別的64MB地址空間,命名為“zygote/non moving space”,
2 Zygote行程此時可在“zygote/non moving space”和RegionSpace中分配物件,
3 當Fork SystemServer時,Zygote行程進行一次Full GC并Trim裁剪記憶體,將回收后的記憶體還給系統,再將RegionSpace中存活物件復制到“zygote/non moving space”,將兩部分的存活物件進行一次壓縮,整個64MB空間一拆為二,壓縮后保留Zygote行程所有存活物件的Space命名為“zygote space”,剩余的空閑記憶體命名為“non moving space”,
C)一拆為二后,新的“zygote space”不再支持記憶體分配與釋放,
(4)Non moving space [61M]:
A) 分配演算法:Dlmalloc,
B) 存放AllocNonMovableObject分配的obj,主要是類加載程序創建的類物件(Class)、類方法物件(ArtMethod)、類成員變數物件(ArtField),
C) 創建程序見2.2.3,
(5) LOS-LargeObjectSpace [512M]:
A) LOS有兩種實作型別(free list\mmap),Android R默認實作為free list形式,以Heap size(capacity_)作為引數呼叫FreeListSpace::Create()介面創建,
B) 單次分配超過12KB的String和基礎型別的陣列如int[],在LOS中分配,
三、GC的常見型別
1. GC流程簡介
GC相關博客文章已經浩如煙海,推薦讀者閱讀鄧凡平老師的《深入理解Android:java虛擬機ART》、周志明老師的《深入理解java虛擬機》和網上的博客例如羅升陽、蘆航等,本文不做鋪陳,
2. GC型別與案例
為了反映不同場景下的GC,作者使用GC對應用狀態的影響和gc_cause(觸發原因)來做區分,能夠更好的闡述GC不同型別之間的異同,
基于GC對應用狀態的影響將GC初步分為兩類,并發類和阻塞類,并發類GC指GC在GC回收執行緒(HeapTaskDaemon)執行,阻塞類GC在行程的作業執行緒執行,
再根據gc_cause具體分為Background GC\Native GC\CollectorTransition GC\Alloc GC\Explicit GC,
(1) 并發類GC
并發類GC因為運行在HeapTaskDaemon執行緒中,對應用的狀態影響較小,一般情況下對于用戶體驗和應用邏輯是透明的,并發類GC對于記憶體回收具有至關重要的作用,減少系統處于低記憶體和大量記憶體碎片的情況,對于如今的手機SOC算力和配置來說,CPU能力比較富余,而記憶體不太富余,應當在合適的時機多執行并發類GC,
但是在一些特殊場景如某些特殊應用、低端手機中,因并發類GC帶來的卡頓、高負載、功耗問題也很常見,主要的影響有以下幾類,
(A) HeapTaskDaemon運行GC時,CC演算法需要很強算力,容易搶占CPU大核\超大核資源,導致應用UI繪制相關執行緒task無法及時被CPU調度或UI繪制相關執行緒被擠在小核中無法在一個Vsync周期完成繪制與渲染,進而引起卡頓不流暢,
(B) 后臺駐留應用多,且后臺活動頻繁時,多個后臺HeapTaskDaemon占用多個CPU核,加重負載情況,引起整機高負載,在整機高負載下各行程的執行緒存在各種Runnable和Uninterruptable sleep(D)狀態,各種SystemServer等鎖、ANR、IOwait、BlockMsg情形接踵而至,卡頓也就逃不掉了,
(C) CC回收演算法STW(stop the world)時間即pause time已經非常低,但是依然存在一些場景下,因CC演算法某些階段需要STW(比如ReclaimPhase階段需要LockHeap鎖堆,鎖堆會STW),造成行程除HeapTaskDaemon外的其他執行緒都處于Sleeping狀態,特殊應用可能出現STW時間達到幾十ms級別以上,會導致卡頓問題,見3.2.1.1.1點,
(D) Blocking GC導致丟幀,例如主執行緒呼叫System.gc()進行一次顯示GC,因ART虛擬機一次只能發起一次GC,若此時有其他GC在運行,需等待此次GC完成,主執行緒一直等待導致丟幀卡頓,
(E) 特殊應用在用戶操作時臨時物件分配記憶體過多,HeapTaskDaemon持續進行GC,GC頻繁導致場景功耗電流很高,影響續航與發熱,
① Background GC/Bg GC
此類為最常見的GC型別,大致90%以上的GC都是此型別,
(A) 觸發邏輯:ART 中heap.cc維護一個Background GC水位值(concurrent_start_bytes_),此水位值由以下因素(multiplier\maxfree\minfree\utilization\gctype)共同影響,在每次GC完成后,呼叫GrowForUtilization()介面計算Background GC水位值,
GC水位計算比較簡單,讀者可百度GC觸發時機相關文章,本文不再鋪陳,
在每次物件分配后,判斷已分配java堆大小和水位的對比,若超水位立刻觸發Background GC,
(B) 作用:時刻追蹤java堆大小,避免垃圾物件過多,及時回收記憶體,減少不必要的記憶體消耗,
1) 案例1【前臺應用Bg GC頻繁,引起主觀滑動卡頓,頓挫感明顯】
應用:新浪新聞V7.63.1
場景:視頻>>小視頻>>短視頻展示頁,上下滑動,明顯卡頓
systrace情形:HeapSize呈鋸齒狀,HeapTaskDaemon中GC頻繁,Sticky/Full GC互相交替運行,
卡頓根因:滑動程序此應用臨時物件占用記憶體過多,堆Heap size上升迅速,達到GC水位線,觸發Bg GC,GC回收ReclaimPhase階段lock heap鎖堆各執行緒被pause,主執行緒Sleeping耗時過長丟幀卡頓,
優化方向:應用記憶體使用不合理,反饋應用優化,

2) 案例2【后臺應用Bg GC頻繁,引起整機高負載,引起前臺應用卡頓】
場景:后臺駐留20-30個應用,壓力測驗應用啟動和關閉時桌面影片流暢性
Systrace情形:后臺應用活動頻繁,多個行程HeapTaskDaemon行程占用卡頓時間段內30-50% CPU資源,
卡頓根因:后臺應用活動頻繁,引起整機高負載,引起前臺應用的UI相關執行緒資源調度不及時,
優化方向:1 排查后臺管控策略,加強管控能力 2 緩解方案:減少高負載時Bg GC,優化HeapTaskDaemon調度


② Native GC/NativeAlloc GC
ART可以管理一些特定的Native記憶體,某些java類(如Bitmap)使用NativeAllocationRegistry類申請和釋放Native記憶體時會同步告知ART,ART可監控此類由java物件參考的Native物件記憶體,
此類參考模式就像“提線木偶”,java物件參考Native物件,java物件記憶體占用很小,大頭在Native物件,在ART回收java物件時,此java物件已設定死亡回呼會主動釋放參考的Native記憶體,
基于此原理ART虛擬機可間接管理一部分Native記憶體,
##注意,開發人員在C++\C代碼中malloc\mmap分配的Native記憶體,無法被ART管理,此部分記憶體使用沒有上限,只能依靠LMKD或虛擬地址空間耗盡OOM,行程死亡后回收,
(A) 觸發邏輯:
1 單次超過300KB的Native記憶體分配,觸發Native已分配記憶體和Native GC水位檢查,超過則進行一次NativeGC,
2 每32次小于300KB的Native記憶體分配,觸發一次檢查,超過Native GC水位進行一次NativeGC,
(B) 與Background GC異同點:GC代碼流程完全一樣,主要為觸發原因不同,Bg GC回收Bitmap,也會自動釋放Bitmap關聯的Native記憶體,
③ 案例1【Native GC頻繁引起特定場景高負載】
場景:應用內退出到桌面、特定應用(如相機、相冊)冷熱啟動
Systrace表現:應用行程HeapTaskDaemon執行緒運行gc_cause為NativeAlloc的GC,增加系統負載,
優化方向:simplePerf確認觸發堆疊,推動優化,若為超過300KB的NativeGC,強烈建議優化掉,減少必現的GC,

④ CollectorTransition GC
此類GC發生在應用processState在IMPORTANT_FOREGROUND(6)狀態上下切換時,processState<=6代表應用可被感知,>6代表應用活動不被感知,ART中只關心是否能被感知,進而調整一些GC的引數,在ART設計中,應用processState>6時,代表應用退入后臺用戶無法感知(STW時間不關心),此時應進行記憶體回收和整理,減少記憶體碎片,提升記憶體利用率,CollectorTransition GC就是起此作用,
(A) 觸發邏輯:應用退入后臺即processState>6,代表用戶無法感知此應用,請求一次delay 5s的并發GC,gc_cause為kGcCauseCollectorTransition,
(B) 與Background GC異同點:GC邏輯流程一致,但是CC回收器對Explicit GC和CollectorTransition GC會進行最大程度的記憶體整理,所有region會被拷貝至tospace,不受75%比例的限制,存活物件整理到新的region中,
⑤ 案例1【CollectorTransition GC頻繁引起應用退出影片卡頓】
應用:騰訊視頻
場景:后臺駐留20-30個應用,壓力測驗應用開啟與關閉時的流暢性
Systrace表現:桌面啟動應用時影片不流暢,整機負載偏高,Heaptaskdaemon為負載TOP1,主要占據4個小核,
卡頓根因:GC負載主因是騰訊視頻退后臺,延遲5S后觸發CollectorTransition型別的GC,且騰訊視頻同個UID下5個行程同步觸發,剛好遇上桌面啟動應用程序,另外其他應用的GC也有一定負載影響,
優化方向:1 加強后臺行程管控(退后臺快速凍結,限制資源使用等)2 保證記憶體壓縮效果情況下減少CollectorTransition GC,


(2) 阻塞類GC
作者將運行在作業執行緒中的GC歸類為阻塞類GC,主要有兩類:Alloc GC和Explicit GC,阻塞類GC會Block作業執行緒,可能出現GC耗時過長,導致行程出現操作卡頓、ANR、卡死等情況發生,在應用設計中,應盡可能避免此類GC,
Alloc GC常見于行程heap size觸頂即達到堆上限,無法再繼續分配物件,將發起一次或多次GC回收記憶體再嘗試分配,若依然分配失敗,會觸發OOM行程死亡,
Explicit GC常見于兩種情況,1 應用代碼中主動呼叫Runtime.gc()\System.gc(),主要見于APP覆寫onTrimMemory()介面,2 SystemServer\系統應用\adb除錯等行程給對應行程發送kill -10產生SIGUSER1的signal,此行程的SignalCatcher執行緒捕獲signal后執行一次Explicit GC,
阻塞類GC主要影響有以下幾點:
(A) 阻塞作業執行緒運行,GC耗時普遍在50ms-200ms間甚至更長,APP邏輯運行時間增加,可能引起卡頓問題,
(B) 應用主動頻繁呼叫Explicit GC,可能出現回收效果甚微,但增加大量無效負載,引起功耗增加,續航減少,手機發熱等問題,
(C) 在行程heap size快要觸頂達到堆上限時,一般是記憶體泄漏場景,此時各作業執行緒和HeaptaskDaemon都在發起各種GC,各類GC互相Block等鎖,導致應用卡死,頭部TOP應用比較常見,
① Alloc GC
從專案經驗看,因為最近幾年的Android版本都使用CC回收器,RegionSpace的limit已經設定成堆上限值,出現Alloc GC即可代表heap size接近觸頂快到達堆上限,幾乎無法回收出記憶體,
絕大部分都是應用出現記憶體泄漏,此時多次Alloc GC也釋放不出記憶體,出現各種Block等鎖,應用卡死,擠牙膏擠不出,或者只能擠一點點,等待此行程的要不卡死要不就是OOM,
以下兩個案例介紹這種情形,
1) 案例1【Alloc GC頻繁引起應用操作卡頓】
應用:內部測驗工具
場景:測驗工具持續壓力使用,應用長時間卡頓定屏
Systrace表現:如下圖,heapsize已經達到242M,此時分配大記憶體物件失敗,主執行緒觸發多次Alloc GC,因記憶體泄漏,并未回收出記憶體,應用馬上出現OOM閃退,
卡頓根因:應用記憶體泄漏,
優化方向:1 解決記憶體泄漏點 2 若應用必須占用大量記憶體,可配置android:largeHeap="true" 3 fork新行程實作功能,避免單個行程heap size過高

2) 案例2【Block GC頻繁引起應用卡死】
應用:抖音
場景:長時間壓力測驗抖音視頻切換
問題特征:
1 Log:卡死期間一直列印Starting a blocking GC xx和WaitForGcToComplete blocked xx on xx for xxms
2 Systrace:Heap size觸頂,且各執行緒有很多TAG(GC:Wait For Completion xx)
3 Meminfo:行程此時記憶體占用2G以上
卡死根因:抖音記憶體泄漏,各作業執行緒發起Alloc GC,但HeapTaskDaemon正在運行GC,各作業執行緒等鎖Sleeping,造成卡死,
優化方向:推動應用優化記憶體泄漏(已反饋,新版本已優化)


② Explicit GC/顯式GC/強制GC/
見3.2.2阻塞類GC的闡述,此類GC多見于應用主動呼叫Runtime.gc()\System.gc(),以下兩個案例分別為OnTrimMemory呼叫顯式GC和作業執行緒中主動呼叫顯式GC,
##Explicit GC和CollectorTransition GC一樣,沒有MakingPhase階段,因為系統設定兩種GC都會做最大力度的記憶體整理,所有region的存活物件都會被拷貝,不受75%存活量占比限制,

1)案例1【OnTrimMemory 中Explicit GC頻繁引起應用卡頓】
應用:平行空間、bima+
場景:后臺駐留35應用,自動化用例運行24小時,檢查各行程丟幀與系統各指標
特征: systrace檔案中發現doFrame的commit callback中執行trimMemory,執行一次concurrent copying GC,導致此次doFrame耗時過長,丟幀卡頓,
根因: 系統低記憶體觸發OnTrimMemory回呼,應用在OnTrimMemory中直接呼叫顯式GC介面,
優化方向:1 系統分析優化低記憶體場景 2 應用實作OnTrimMemory因根據trimLevel和自身應用狀態,根據情況按需呼叫顯式GC介面,并且呼叫操作在子執行緒中實作,

2) 案例2【微信視頻號/視頻直播頁面Explicit GC頻繁】
應用:微信 V8.0.11
場景:
1 發現》直播和附近》直播》任意打開一個直播》上下滑動切換,必現一次顯式GC
2 發現》點擊視頻號,必現一次顯式GC
3 點擊好友頭像》進入好友詳情頁,點擊好友的視頻號,必現一次顯式GC
優化方向:google已明確不建議應用呼叫顯式GC來回收記憶體,Bg GC已經可以很好的回收垃圾物件,應用邏輯必現的顯式GC,應優化記憶體分配邏輯,去除顯式GC呼叫,減少無效負載,
當前此問題正和微信對接中,



四、總結
本文從Android R版本GC相關配置為入口,以HelloWorld應用為例介紹Heap布局,能夠讓讀者對Android R版本java記憶體分配和管理有一個概念認識,緊接著以實際GC型別和案例,介紹各種不同gc cause GC的觸發邏輯和影響,為系統開發人員和APP客戶端開發人員提供專案GC問題優化思路,
參考資料
1) Android R原始碼
2) 《深入理解android java虛擬機art-鄧凡平》
3) 老羅的Android之旅-https://blog.csdn.net/Luoshengyang?t=1&type=blog
4) ART的堆記憶體布局-https://www.cnblogs.com/YYPapa/p/6851299.html
5) ART虛擬機 | 如何讓GC同步回收native記憶體-https://juejin.cn/post/6894153239907237902
6)Android GC 簡史-https://juejin.cn/post/6966205309782065159

長按關注
內核工匠微信
Linux 內核黑科技 | 技術文章 | 精選教程
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/304960.html
標籤:其他
