Android ART 垃圾回收機制

平時有在看一些jvm的gc機制,但作爲一名android開發者,竟然沒有了解過android的垃圾回收機制,雖然android也是用java虛擬機跑app,但畢竟android用的是ART(5.0以後),不是原生的jvm,那androd的垃圾回收機制和jvm的是不是一樣呢。今天就來學習一下,此文主要是翻譯google的官方文檔,原文鏈接如下
Debugging ART Garbage Collection

ART GC概覽


ART 是在 Android 4.4 中引入的一個開發者選項,也是 Android 5.0 及更高版本的默認 Android 運行時。目前google已經不再維護和發佈dalvik。

ART 有多個不同的 GC 方案,這些方案包括運行不同垃圾回收器。默認方案是 CMS(併發標記清除)方案,主要使用粘性CMS和部分CMS。粘性CMS是ART的不移動分代垃圾回收器。它僅掃描堆中自上次GC後修改的部分,並且只能回收自上次GC後分配的對象。除CMS方案外,當應用將進程狀態更改爲察覺不到卡頓的進程狀態(例如,後臺或緩存)時,ART將執行堆壓縮

除了新的垃圾回收器之外,ART 還引入了一種基於位圖的新內存分配器,稱爲 RosAlloc(插槽運行分配器)。此新分配器具有分片鎖,當分配規模較小時可添加線程的本地緩衝區,因而性能優於DlMalloc。

與 Dalvik 相比,ART CMS 垃圾回收計劃在很多方面都有一定的改善:

  • 與 Dalvik 相比,暫停次數從 2 次減少到 1 次。Dalvik 的第一次暫停主要是爲了進行根標記,而這個動作在 ART中已經是讓線程自己去標記,然後馬上恢復運行,這樣就減少了一次暫停。
  • 與 Dalvik 類似,ART GC 在清除過程開始之前也會暫停 1 次。兩者在這方面的主要差異在於:在此暫停期間,某些 Dalvik 環節在 ART 中併發進行。這些環節包括 java.lang.ref.Reference 處理、系統弱清除(例如,jni 弱全局等)、重新標記非線程根和卡片預清理。在 ART 暫停期間仍進行的階段包括掃描髒卡片以及重新標記線程根,這些操作有助於縮短暫停時間。
  • 相對於 Dalvik,ART GC 改進的最後一個方面是粘性 CMS 回收器增加了 GC 吞吐量。不同於普通的分代 GC,粘性 CMS 不移動。系統會將年輕對象保存在一個分配堆棧(基本上是 java.lang.Object 數組)中,而非爲其設置一個專屬區域。這樣可以避免移動所需的對象以維持低暫停次數,但缺點是容易在堆棧中加入大量複雜對象圖像而使堆棧變長。

ART GC 與 Dalvik 的另一個主要區別在於 ART GC 引入了移動垃圾回收器。使用移動 GC 的目的在於通過堆壓縮來減少後臺應用使用的內存。目前,觸發堆壓縮的事件是 ActivityManager 進程狀態的改變。當應用轉到後臺運行時,它會通知 ART 已進入不再“感知”卡頓的進程狀態。此時 ART 會進行一些操作(例如,壓縮和監視器壓縮),從而導致應用線程長時間暫停。目前正在使用的兩個移動 GC 是同構空間壓縮和半空間壓縮。

  • 半空間壓縮將對象在兩個緊密排列的碰撞指針空間之間進行移動。這種移動 GC 適用於小內存設備,因爲它可以比同構空間壓縮稍微多節省一點內存。額外節省出的空間主要來自緊密排列的對象,這樣可以避免 RosAlloc/DlMalloc 分配器佔用開銷。由於 CMS 仍在前臺使用,且不能從碰撞指針空間中進行收集,因此當應用在前臺使用時,半空間還要再進行一次轉換。這種情況並不理想,因爲它可能引起較長時間的暫停。
  • 同構空間壓縮通過將對象從一個 RosAlloc 空間複製到另一個 RosAlloc 空間來實現。這有助於通過減少堆碎片來減少內存使用量。這是目前非低內存設備的默認壓縮模式。相比半空間壓縮,同構空間壓縮的主要優勢在於應用從後臺切換到前臺時無需進行堆轉換。

GC 驗證和性能選項


您可以採用多種方法來更改 ART 使用的 GC 計劃。更改前臺 GC 計劃的主要方法是更改 dalvik.vm.gctype 屬性或傳遞 -Xgc: 選項。您可以通過以逗號分隔的格式傳遞多個 GC 選項。

爲了導出可用 -Xgc 設置的完整列表,可以鍵入 adb shell dalvikvm -help 來輸出各種運行時命令行選項。

以下是將 GC 更改爲半空間並打開 GC 前堆驗證的一個示例: adb shell setprop dalvik.vm.gctype SS,preverify

  • CMS 也是默認值,指定併發標記清除 GC 計劃。該計劃包括運行粘性分代 CMS、部分 CMS 和完整 CMS。該計劃的分配器是適用於可移動對象的 RosAlloc 和適用於不可移動對象的 DlMalloc。
  • SS 指定半空間 GC 計劃。該計劃有兩個適用於可移動對象的半空間和一個適用於不可移動對象的 DlMalloc 空間。可移動對象分配器默認設置爲使用原子操作的共享碰撞指針分配器。但是,如果 -XX:UseTLAB 標記也被傳入,則分配器使用線程局部碰撞指針分配。
  • GSS 指定分代半空間計劃。該計劃與半空間計劃非常相似,但區別在於其會將存留期較長的對象提升到大型 RosAlloc 空間中。這樣就可明顯減少典型用例中需複製的對象。

堆驗證

堆驗證可能是調試 GC 相關錯誤或堆損壞的最有用的 GC 選項。啓用堆驗證會使 GC 在垃圾回收過程中在幾個點檢查堆的正確性。堆驗證與更改 GC 類型的選項相同。啓用堆驗證後,堆驗證會驗證根,並確保可訪問對象僅引用其他可訪問對象。通過傳入以下 -Xgc 值可以啓用 GC 驗證:

  • 如果啓用 GC 驗證,[no]preverify 將在 GC 啓動之前執行堆驗證。
  • 如果啓用 GC 驗證,[no]presweepingverify 將在啓動垃圾回收器清除過程之前執行堆驗證。
  • 如果啓用 GC 驗證,[no]postverify 將在 GC 完成清除後執行堆驗證。
  • [no]preverify_rosalloc、[no]postsweepingverify_rosalloc 和 [no]postverify_rosalloc 也是附加 GC 選項,僅驗證 RosAlloc 內部計算的狀態。驗證的主要內容是,魔數值是否與預期常量匹配,以及可用內存塊是否已在 free_page_runs_ 映射中註冊。

使用 TLAB 分配器選項

目前,只有 TLAB 選項可以更改分配器而不影響活動 GC 類型。此選項不可通過系統屬性使用,但可以通過將
-XX:UseTLAB 傳遞給 dalvikvm 來啓用。該選項的分配代碼路徑更短,因此分配速度更快。由於此選項需要使用暫停時間相當長的 SS 或 GSS GC 類型,因此默認情況下不啓用。

性能


評測 GC 性能主要使用兩種工具:GC 時間轉儲和 systrace。評測 GC 性能問題的最直觀方法是使用 systrace 確定哪些 GC 會導致長時間暫停或搶佔應用線程。儘管 ART GC 效率相對較高,但是過度分配或錯誤的變異器行爲等都能造成性能問題。

人機工程學

與 Dalvik 相比,ART 在 GC 人機工程學方面存在一些重要差異。與 Dalvik 相比,其中一項重要改進是在我們延後啓用並行 GC 時不再分配 GC。但是,此操作存在一個缺點:在某些情況下,不阻止 GC 會導致堆的增長速度比 Dalvik 快。好在 ART 具有堆壓縮功能,可在進程變爲後臺進程狀態時對堆進行碎片整理來緩解此問題。

CMS GC 人機工程學有兩種定期運行的 GC。理想情況下,GC 人機工程學更多時間運行的是分代粘性 CMS,而非部分 CMS。GC 將一直運行粘性 CMS,直到最後一個 GC 的吞吐量(通過釋放的字節數/GC 持續秒數計算得出)小於部分 CMS 的平均吞吐量。發生此情況時,人機工程學將下一個併發 GC 計劃爲部分 CMS,而非粘性 CMS。部分 CMS 完成後,人機工程學將下一個 GC 更改回粘性 CMS。粘性 CMS 在完成後不會調整堆佔用空間限制,這是促使人機工程學發揮作用的一個關鍵因素。這樣,粘性 CMS 的發生頻率更高,直到吞吐量低於部分 CMS,最終導致堆增大。

使用 SIGQUIT 獲取 GC 性能信息

通過將 SIGQUIT 發送到已運行的應用,或在啓動命令行程序時通過將 -XX:DumpGCPerformanceOnShutdown 傳遞給 dalvikvm,可以獲得應用的 GC 性能時序。當應用獲得 ANR 請求信號 (SIGQUIT) 時,它將轉儲與鎖定、線程堆棧和 GC 性能相關的信息。

獲取 GC 時序轉儲的方法是使用以下命令:

adb shell kill -S QUIT PID

該操作將在 /data/anr/ 中創建一個 traces.txt 文件。此文件包含一些 ANR 轉儲信息以及 GC 時序。您可以通過搜索“轉儲累計 GC 時序”來確定 GC 時序。這些時序會顯示一些相關內容。它會顯示各 GC 類型的階段和暫停的直方圖信息。暫停信息通常比較重要。例如:

sticky concurrent mark sweep paused: Sum: 5.491ms 99% C.I. 1.464ms-2.133ms Avg: 1.830ms Max: 2.133ms

This 顯示暫停的平均時間爲 1.83 ms。該值足夠低,在大多數應用中不會導致丟幀,因此無需擔心。
需要關注的另一個問題是掛起時間。掛起時間測量在 GC 要求某個線程掛起後,線程到達掛起點所需的時間。該時間包含在 GC 暫停中,所以可用於確定長時間暫停是否是由 GC 緩慢或線程掛起緩慢造成。以下是 Nexus 5 上的正常掛起時間示例:

suspend all histogram: Sum: 1.513ms 99% C.I. 3us-546.560us Avg: 47.281us Max: 601us

還有一些其他要關注的方面,例如總耗時、GC 吞吐量等。示例如下:

Total time spent in GC: 502.251ms
Mean GC size throughput: 92MB/s
Mean GC object throughput: 1.54702e+06 objects/s

轉儲已運行應用的 GC 時序的示例如下:

adb shell kill -s QUIT PID
adb pull /data/anr/traces.txt

此時,GC 時序包含在 trace.txt 中。Google 地圖的輸出示例如下:

Start Dumping histograms for 34 iterations for sticky concurrent mark sweep
ScanGrayAllocSpaceObjects: Sum: 196.174ms 99% C.I. 0.011ms-11.615ms Avg: 1.442ms Max: 14.091ms
FreeList: Sum: 140.457ms 99% C.I. 6us-1676.749us Avg: 128.505us Max: 9886us
MarkRootsCheckpoint: Sum: 110.687ms 99% C.I. 0.056ms-9.515ms Avg: 1.627ms Max: 10.280ms
SweepArray: Sum: 78.727ms 99% C.I. 0.121ms-11.780ms Avg: 2.315ms Max: 12.744ms
ProcessMarkStack: Sum: 77.825ms 99% C.I. 1.323us-9120us Avg: 576.481us Max: 10185us
(Paused)ScanGrayObjects: Sum: 32.538ms 99% C.I. 286us-3235.500us Avg: 986us Max: 3434us
AllocSpaceClearCards: Sum: 30.592ms 99% C.I. 10us-2249.999us Avg: 224.941us Max: 4765us
MarkConcurrentRoots: Sum: 30.245ms 99% C.I. 3us-3017.999us Avg: 444.779us Max: 3774us
ReMarkRoots: Sum: 13.144ms 99% C.I. 66us-712us Avg: 386.588us Max: 712us
ScanGrayImageSpaceObjects: Sum: 13.075ms 99% C.I. 29us-2538.999us Avg: 192.279us Max: 3080us
MarkingPhase: Sum: 9.743ms 99% C.I. 170us-518us Avg: 286.558us Max: 518us
SweepSystemWeaks: Sum: 8.046ms 99% C.I. 28us-479us Avg: 236.647us Max: 479us
MarkNonThreadRoots: Sum: 5.215ms 99% C.I. 31us-698.999us Avg: 76.691us Max: 703us
ImageModUnionClearCards: Sum: 2.708ms 99% C.I. 26us-92us Avg: 39.823us Max: 92us
ScanGrayZygoteSpaceObjects: Sum: 2.488ms 99% C.I. 19us-250.499us Avg: 37.696us Max: 295us
ResetStack: Sum: 2.226ms 99% C.I. 24us-449us Avg: 65.470us Max: 452us
ZygoteModUnionClearCards: Sum: 2.124ms 99% C.I. 18us-233.999us Avg: 32.181us Max: 291us
FinishPhase: Sum: 1.881ms 99% C.I. 31us-431.999us Avg: 55.323us Max: 466us
RevokeAllThreadLocalAllocationStacks: Sum: 1.749ms 99% C.I. 8us-349us Avg: 51.441us Max: 377us
EnqueueFinalizerReferences: Sum: 1.513ms 99% C.I. 3us-201us Avg: 44.500us Max: 201us
ProcessReferences: Sum: 438us 99% C.I. 3us-212us Avg: 12.882us Max: 212us
ProcessCards: Sum: 381us 99% C.I. 4us-17us Avg: 5.602us Max: 17us
PreCleanCards: Sum: 363us 99% C.I. 8us-17us Avg: 10.676us Max: 17us
ReclaimPhase: Sum: 357us 99% C.I. 7us-91.500us Avg: 10.500us Max: 93us
(Paused)PausePhase: Sum: 312us 99% C.I. 7us-15us Avg: 9.176us Max: 15us
SwapBitmaps: Sum: 166us 99% C.I. 4us-8us Avg: 4.882us Max: 8us
(Paused)ScanGrayAllocSpaceObjects: Sum: 126us 99% C.I. 14us-112us Avg: 63us Max: 112us
MarkRoots: Sum: 121us 99% C.I. 2us-7us Avg: 3.558us Max: 7us
(Paused)ScanGrayImageSpaceObjects: Sum: 68us 99% C.I. 68us-68us Avg: 68us Max: 68us
BindBitmaps: Sum: 50us 99% C.I. 1us-3us Avg: 1.470us Max: 3us
UnBindBitmaps: Sum: 49us 99% C.I. 1us-3us Avg: 1.441us Max: 3us
SwapStacks: Sum: 47us 99% C.I. 1us-3us Avg: 1.382us Max: 3us
RecordFree: Sum: 42us 99% C.I. 1us-3us Avg: 1.235us Max: 3us
ForwardSoftReferences: Sum: 37us 99% C.I. 1us-2us Avg: 1.121us Max: 2us
InitializePhase: Sum: 36us 99% C.I. 1us-2us Avg: 1.058us Max: 2us
FindDefaultSpaceBitmap: Sum: 32us 99% C.I. 250ns-1000ns Avg: 941ns Max: 1000ns
(Paused)ProcessMarkStack: Sum: 5us 99% C.I. 250ns-3000ns Avg: 147ns Max: 3000ns
PreSweepingGcVerification: Sum: 0 99% C.I. 0ns-0ns Avg: 0ns Max: 0ns
Done Dumping histograms
sticky concurrent mark sweep paused: Sum: 63.268ms 99% C.I. 0.308ms-8.405ms
Avg: 1.860ms Max: 8.883ms
sticky concurrent mark sweep total time: 763.787ms mean time: 22.464ms
sticky concurrent mark sweep freed: 1072342 objects with total size 75MB
sticky concurrent mark sweep throughput: 1.40543e+06/s / 98MB/s
Total time spent in GC: 4.805s
Mean GC size throughput: 18MB/s
Mean GC object throughput: 330899 objects/s
Total number of allocations 2015049
Total bytes allocated 177MB
Free memory 4MB
Free memory until GC 4MB
Free memory until OOME 425MB
Total memory 90MB
Max memory 512MB
Zygote space size 4MB
Total mutator paused time: 229.566ms
Total time waiting for GC to complete: 187.655us

分析 GC 正確性問題的工具


造成 ART 內部崩潰的原因多種多樣。讀取或寫入對象字段時出現崩潰可能表示存在堆損壞。如果 GC 在運行時崩潰,也可能是由堆損壞造成的。造成堆損壞的原因多種多樣,最常見的原因可能是應用代碼錯誤。好在可以使用相關工具調試與 GC 和堆相關的崩潰問題。此類工具包括上面指定的堆驗證選項、valgrind 和 CheckJNI。

CheckJNI

驗證應用行爲的另一種方法是使用 CheckJNI。CheckJNI 是一種添加額外 JNI 檢查的模式;出於性能考慮,這些選項在默認情況下並不會啓用。此類檢查將捕獲一些可能導致堆損壞的錯誤,例如使用無效/過時的局部和全局引用。啓用 CheckJNI 的方法如下:

adb shell setprop dalvik.vm.checkjni true

Forcecopy 模式是 CheckJNI 的另一部分,對檢測超出數組區域末端的寫入非常有用。啓用後,forcecopy 會促使數組訪問 JNI 函數時始終返回帶有紅色區域的副本。紅色區域是返回指針結束/開始處的一個區域,該區域具有特殊值,並在數組釋放時得到驗證。如果紅色區域中的值與預期值不匹配,則通常意味着發生緩衝區溢出或欠載。這將導致 CheckJNI 中止。啓用 forcecopy 模式的方法如下:

adb shell setprop dalvik.vm.jniopts forcecopy

CheckJNI 應捕獲錯誤的一個示例是超出從 GetPrimitiveArrayCritical 獲取的數組末端的寫入。該操作很可能會破壞 Java 堆。如果寫入位於 CheckJNI 紅色區域內,則在調用相應 ReleasePrimitiveArrayCritical 時,CheckJNI 將會捕獲該問題。否則,寫入將最終損壞 Java 堆中的一些隨機對象,並可能會導致之後出現 GC 崩潰。如果崩潰的內存是引用字段,則 GC 可能會捕獲錯誤並輸出“Tried to mark not contained by any spaces”這一錯誤消息。

當 GC 嘗試標記無法找到空間的對象時,就會發生此錯誤。在此檢查失敗後,GC 會遍歷根,並嘗試查看無效對象是否爲根。結果共有兩個選項:對象爲根或非根。

Valgrind

ART 堆支持可選的 valgrind 工具,這款工具提供了一種方法來檢測對無效堆地址的讀取和寫入操作。ART 可檢測應用何時在 valgrind 下運行,並在每個對象分配前後插入紅色區域。如果對這些紅色區域有任何讀取或寫入錯誤,valgrind 將輸出錯誤消息。例如,如果您在通過 JNI 直接訪問數組時,越過數組元素末端進行讀取或寫入,就會出現此類錯誤。由於 AOT 編譯器使用隱式 null 檢查,因此建議使用 eng 版本運行 valgrind。另外值得一提的是,valgrind 比正常執行速度慢一個數量級。

以下是一個使用示例:

# build and install
mmm external/valgrind
adb remount && adb sync
# disable selinux
adb shell setenforce 0
adb shell setprop wrap.com.android.calculator2
“TMPDIR=/data/data/com.android.calculator2 logwrapper valgrind”
#push symbols
adb shell mkdir /data/local/symbols
adb push $OUT/symbols /data/local/symbols
adb logcat

無效的根示例

如果對象實際上爲無效根,則會輸出一些有用信息:art E 5955 5955 art/runtime/gc/collector/mark_sweep.cc:383] Tried to mark 0x2 not contained by any spaces

art E 5955 5955 art/runtime/gc/collector/mark_sweep.cc:384] Attempting see if
it’s a bad root
art E 5955 5955 art/runtime/gc/collector/mark_sweep.cc:485] Found invalid
root: 0x2
art E 5955 5955 art/runtime/gc/collector/mark_sweep.cc:486]
Type=RootJavaFrame thread_id=1 location=Visiting method ‘java.lang.Object
com.google.gwt.corp.collections.JavaReadableJsArray.get(int)’ at dex PC 0x0002
(native PC 0xf19609d9) vreg=1

在這種情況下,vreg 1(在 com.google.gwt.corp.collections.JavaReadableJsArray.get 內)應該包含一個堆引用,但實際上卻包含了地址 0x2 的一個無效指針。這顯然是一個無效根。要調試此問題,下一步是在 oat 文件中使用 oatdump,並查看具有無效根的方法。在這種情況下,結果證明錯誤在於x86後端的編譯器錯誤。修正該錯誤的更改列表如下:https://android-review.googlesource.com/#/c/133932/

損壞的對象示例

如果對象不是根,則會輸出類似於以下輸出內容的消息:

01-15 12:38:00.196 1217 1238 E art : Attempting see if it’s a bad root
01-15 12:38:00.196 1217 1238 F art :
art/runtime/gc/collector/mark_sweep.cc:381] Can’t mark invalid object

當堆損壞不是無效根時,將很難進行調試。此錯誤消息表示堆中至少含有一個指向無效對象的對象。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章