還由於運行卡頓被懟?教你垃圾回收的黑科技!

今天,咱們來學習下 Android 中的垃圾回收機制。git

你們應該知道,JVM 和 Dalvik 的垃圾回收機制實際並不徹底相同。而垃圾回收機制一直都是工做和麪試中的必備技能,對 GC 有深刻的理解,才能在代碼層面更好地去減小 GC 的發生,畢竟每次 GC 都會對主線程的運行形成必定的卡頓,從而影響到用戶體驗。github

1前言

本節將介紹支付寶 Android 客戶端啓動速度優化下的「垃圾回收」具體思路。面試

應用啓動時間是移動 App 一個重要的用戶體驗環節,相對於普通的移動 App,支付寶過於龐大,必然會影響啓動速度,一些常規的優化手段在支付寶中已經作得比較完善了,本篇文章嘗試從 GC 的層面來進一步優化支付寶的啓動速度。性能優化

2背景

相對於 C 語言來講,Java 語言有一些特性,例如開發人員不用考慮內存的分配和回收,然而,進程內存管理又是必不可少的環節,妥協的結果是 Java 語言的設計者們把對象分配和回收放到了 Java虛擬機,這裏但願明確一個概念:GC 是有代價的,這個代價包括:阻塞 Java 程序的執行,佔用 CPU 資源,佔用額外內存等,谷歌的工程師意識到了 GC 對應用的影響,因此把 GC 的日誌默認輸出到了 Logcat,咱們常常可以看到 Logcat 裏輸出如下幾種 GC 日誌:架構

1.GC_EXPLICIT: Dalivk 給開發人員提供的主動觸發 GC 的 API,讀者能夠參看 Google Maps 的設計來體會這個 API 的用法框架

2.GC_FOR _ALLOCK: 是分配對象失敗時觸發的 GC,這個 GC 會將應用全部的 Java 線程暫停運行,直到 GC 結束。ide

3.GC_CONCURRENT: 是 Java 虛擬機根據堆的當前狀態觸發的 GC,這個 GC 在 Dalvik 單獨 GC 線程裏運行,在部分時間裏不影響應用 Java 線程的運行。 支付寶啓動是一個典型的關鍵路徑場景,咱們但願看到儘量少的 GC_ CONCURRENT(若是可能, GC_ FOR_ ALLOCK 也應該縮減到最少),然而,經過 Logcat 咱們會看到很是糟糕的 GC 行爲—大量的 GC_ FOR_ ALLOCK 以及觸目驚心的 Java 線程被 WAIT_ FOR_ CONCURRENT_ GC 阻塞。以下圖所示,經過簡單統計這些 GC 消耗的時間,咱們可以得出 GC 嚴重影響應用啓動時間的結論。函數

3設計思路

支付寶是 Android 系統的一個應用程序,如何可以經過影響 Dalvik 的 GC 行爲來縮短啓動時間呢?這個問題能夠分解爲兩步:組件化

1.支付寶是否能影響自身 Dalvik 的行爲post

2.如何改進 Dalvik,縮短啓動時間

第一個問題答案是確定的,Android 系統的設計思路是每一個 Android 應用程序都有獨立的 Dalvik 實例,應用啓動後能夠修改本身的進程空間裏的代碼和數據,所以支付寶經過修改內存中的 Dalvik 庫文件 libdvm.so 影響 Dalvik 的行爲。

第二個問題的難點在於投入產出比:修改進程空間的代碼和數據是面向二進制,難度遠遠大於源代碼,也就是說稍微複雜的Dalvik改進工做是不可能的。

基於以上兩點,提出了一種設想:啓動時 GC 抑制,容許堆一直增加,直到開發人員主動中止 GC 抑制或者 OOM 中止 GC 抑制,這是一種"空間換時間"策略,用更多的內存消耗來換取啓動時間的縮短,這種策略可行有兩個前提:一是設備廠商沒有加密內存中的 Dalvik 庫文件,二是設備廠商沒有改動 Google 的 Dalvik 源碼(或者少許的改動),理論上經過白名單的方式能夠覆蓋全部設備,可是實現和維護成本都很是高。

4GC 抑制的實現

GC 抑制的前提是 Dalvik 比較熟悉,知道如何改變 GC 的行爲,解決方案大體以下:首先在源碼級別找到抑制GC的修改方法,例如改變跳轉分支,其次,在二進制代碼裏找到 A 分支條件跳轉的"指令指紋",以及用於改變分支的二進制代碼,假設爲 override_A,應用啓動後掃描內存中的 libdvm.so,根據"指令指紋"定位到修改位置,而後用 override_A 覆蓋,這裏須要注意的是,"指令指紋"的定義須要有一些編譯器和 arm 指令集知識,實現 GC 抑制主要實現瞭如下 4 個部分:

  • 取消 softlimit 檢測

  • 取消 GC 線程的喚醒

  • 取消 GC 例程函數

  • OOM 中止 GC 抑制的實現

  • 取消 softlimit 檢測:

取消 softlimit 檢測的目的是最大限度的分配對象,下圖爲 softlimit 檢查對應的 arm 指令片斷,位於 dvmHeapSourceAlloc 函數中,OXE057 對應於"return NULL"的分支,若是咱們想永遠不進入"return NULL"分支,能夠改變 cmp 指令的結果,在具體實現裏咱們把"0X42"做爲"指令指紋"來識別並且修改成 "cmp r0, r0",這樣就能夠實現取消 softlimit 檢查。

  • 取消 GC 線程喚醒

取消 GC 線程喚醒的目的是防止 GC 線程頻繁喚醒致使的線程抖動。下圖是對應的 C++ 代碼和 arm 指令片斷,這段代碼一樣位於 dvmHeapSourceAlloc 函數中。在具體實現裏咱們會依次掃描 libdvm.so的 dynstr、dynsym、rel.plt 和 plt 區域獲取 pthreadcondsignal@plt 的地址,而後遍歷 dvmHeapSourceAlloc 中的全部分支跳轉,計算跳轉目的地址。

若是發現 pthreadcondsignal@plt 和當前分支跳轉目的地址配置,擦除這條指令便可。

  • 取消 GC 例程函數

取消 GC 例程函數採用鉤子技術來實現,咱們將 GC 抑制封裝成了兩個 native 接口 doStartSuppressGCdoStopSuppressGC;而且進一步封裝爲 JNI 接口,便於開發者在 Java 裏調用。通常的應用方式是,開發者經過日誌看到支付寶在某個場景會觸發大量的 GC 且這個 GC 影響用戶體驗(響應時間慢或者動畫卡頓),而後在這個場景先後插入 doStartSuppressGCdoStopSuppressGC

以支付寶冷啓動場景爲例,咱們在容器 Quinox 的 attachBaseContext 函數裏插入 doStartSuppressGC,在首頁加載結束時插入 doStopSuppressGC

  • OOM 中止 GC 抑制的實現

若是僅僅考慮在支付寶啓動過程當中抑制 GC,不須要考慮 OOM 中止 GC 抑制的實現,由於支付寶啓動不足以觸發 OOM。可是咱們但願 GC 抑制成爲一個基礎模塊,可以應用到更多場景中。若是程序在調用 doStopSuppressGC 前觸發了 OOM,則須要在 OOM 發生前中止 GC 抑制。和前面簡單的改變分支跳轉方向不一樣,須要在 OOM 發生前注入一個新的的分支跳轉,這個新分支的代碼由咱們來實現。新分支主要功能是,調用 doStopSuppressGC,而後去掉注入的新分支,最後跳回 Dalvik 執行 OOM。

  • 實現一樣採用傳統的鉤子技術。在鉤子函數 dvmCollectGarbageInternal 裏:

當條件不知足時直接返回,達到取消 GC 的目的;

條件知足時,取消鉤子且執行原來的 dvmCollectGarbageInternal。

實現中使用了開源的二進制注入框架:github.com/crmulliner/…

這裏須要注意的是,在熱點函數裏使用這個框架提供的 pre_hook 和 post_hook 的性能開銷很是大。本文裏的設計只會用到一次 pre_hook,因此不存在性能問題。

看到的這裏讀者可能會問,這種經過「指令指紋」的方式靠譜麼?個人答案是,漏判不影響正確性,誤判理論上存在但機率極小(誤判指「指令指紋」定位到錯誤代碼位置)。即便誤判發生了,咱們還有最後一層保障——基礎架構組同窗實現的容災機制。當誤判致使程序異常沒法完成正常啓動時,重啓支付寶並且在後續的啓動中直接放棄 GC 抑制。

5效果

上圖的啓動時間的數據是在內部的 Android 4.x 測試設備上得到的(沒有標註 release 表示 debug 版本)。從圖表上來看,支付寶客戶端的啓動時間縮短了 15%~30%。

本人Java開發4年Android開發5年,按期分享Android高級技術及經驗分享,歡迎你們關注~(喜歡文章的點個贊鼓勵下叭~謝謝。)

讀者福利

  • Android前沿技術—組件化框架設計

    Android前沿技術

  • BAT主流Android高級架構技術大綱+學習路線+資料分享

架構技術詳解,學習路線與資料分享都在博客這篇文章裏《「寒冬未過」,阿里P9架構分享Android必備技術點,讓你offer拿到手軟!》 (包括自定義控件、NDK、架構設計、混合式開發工程師(React native,Weex)、性能優化、完整商業項目開發等)

  • 全套體系化高級架構視頻;七大主流技術模塊,視頻+源碼+筆記

架構.jpg
相關文章
相關標籤/搜索