江義旺:滴滴出行安卓端 finalize time out 的解決方案

出品 | 滴滴技術java

做者 | 江義旺android


前言:隨着安卓 APP 規模愈來愈大,代碼愈來愈多,各類疑難雜症問題也隨之出現。比較常見的一個問題就是 GC finalize() 方法出現 java.util.concurrent.TimeoutException,這類問題難查難解,困擾了不少開發者。那麼這類問題是怎麼出現的呢?有什麼解決辦法呢?這篇文章爲將探索 finalize() timeout 的緣由和解決方案,分享咱們的踩坑經驗,但願對遇到此類問題的開發者有所幫助。性能優化

在一些大型安卓 APP 中,常常會遇到一個奇怪的 BUG:ava.util.concurrent.TimeoutException
其表現爲對象的 finalize() 方法超時,如 android.content.res.AssetManager.finalize() timed out after 10 seconds 。
此前滴滴出行安卓端曾長期受此 BUG 的影響,天天有一些用戶會所以遇到 Crash,通過深度分析,最終找到有效解決方案。這篇文章將對這個 BUG 的前因後果以及咱們的解決方案進行分析。

多線程

▍問題詳情

finalize() TimeoutException 發生在不少類中,典型的 Crash 堆棧如:
架構


這類 Crash 都是發生在 java.lang.Daemons$FinalizerDaemon.doFinalize 方法中,直接緣由是對象的 finalize() 方法執行超時。系統版本從 Android 4.x 版本到 8.1 版本都有分佈,低版本分佈較多,出錯的類有系統的類,也有咱們本身的類。因爲該問題在 4.x 版本中最具備表明性,下面咱們將基於 AOSP 4.4 源碼進行分析:併發

▍源碼分析

首先從 DaemonsFinalizerDaemon 的由來開始分析,Daemons 開始於 Zygote 進程:Zygote 建立新進程後,經過 ZygoteHooks 類調用了 Daemons 類的 start() 方法,在 start() 方法中啓動了 FinalizerDaemonFinalizerWatchdogDaemon 等關聯的守護線程。函數


Daemons 類主要處理 GC 相關操做,start() 方法調用時啓動了 5 個守護線程,其中有 2 個守護線程和這個 BUG 具備直接的關係。源碼分析

▍FinalizerDaemon 析構守護線程

對於重寫了成員函數finalize()的類,在對象建立時會新建一個 FinalizerReference 對象,這個對象封裝了原對象。當原對象沒有被其餘對象引用時,這個對象不會被 GC 立刻清除掉,而是被放入 FinalizerReference 的鏈表中。FinalizerDaemon 線程循環取出鏈表裏面的對象,執行它們的 finalize() 方法,而且清除和對應 FinalizerReference對象引用關係,對應的 FinalizerReference 對象在下次執行 GC 時就會被清理掉。性能


▍FinalizerWatchdogDaemon 析構監護守護線程

析構監護守護線程用來監控 FinalizerDaemon 線程的執行,採用 Watchdog 計時器機制。當 FinalizerDaemon 線程開始執行對象的 finalize() 方法時,FinalizerWatchdogDaemon 線程會啓動一個計時器,當計時器時間到了以後,檢測 FinalizerDaemon 中是否還有正在執行 finalize() 的對象。檢測到有對象存在後就視爲 finalize() 方法執行超時,就會產生 TimeoutException 異常。
優化


由源碼能夠看出,該 Crash 是在 FinalizerWatchdogDaemon 的線程中建立了一個TimeoutException 傳給 Thread 類的 defaultUncaughtExceptionHandler 處理形成的。因爲異常中填充了 FinalizerDaemon 的堆棧,之因此堆棧中沒有出現和 FinalizerWatchdogDaemon 相關的類。

▍緣由分析

finalize()致使的 TimeoutException Crash 很是廣泛,不少 APP 都面臨着這個問題。使用 finalize() TimeoutException 爲關鍵詞在搜索引擎或者 Stack Overflow 上能搜到很是多的反饋和提問,技術網站上對於這個問題的緣由分析大概有兩種:

▍對象 finalize() 方法耗時較長

finalize() 方法中有耗時操做時,可能會出現方法執行超時。耗時操做通常有兩種狀況,一是方法內部確實有比較耗時的操做,好比 IO 操做,線程休眠等。另外有種線程同步耗時的狀況也須要注意:有的對象在執行 finalize() 方法時須要線程同步操做,若是長時間拿不到鎖,可能會致使超時,如 android.content.res.AssetManager$AssetInputStream 類:


AssetManager 的內部類 AssetInputStream 在執行 finalize() 方法時調用 close() 方法時須要拿到外部類 AssetManager 對象鎖, 而在 AssetManager 類中幾乎全部的方法運行時都須要拿到一樣的鎖,若是 AssetManager 連續加載了大量資源或者加載資源是耗時較長,就有可能致使內部類對象 AssetInputStream 在執行finalize() 時長時間拿不到鎖而致使方法執行超時。


▍5.0 版本如下機型 GC 過程當中 CPU 休眠致使


有種觀點認爲系統可能會在執行 finalize() 方法時進入休眠, 而後被喚醒恢復運行後,會使用如今的時間戳和執行 finalize() 以前的時間戳計算耗時,若是休眠時間比較長,就會出現 TimeoutException。

詳情請見∞

確實這兩個緣由可以致使 finalize() 方法超時,可是從 Crash 的機型分佈上看大部分是發生在系統類,另外在 5.0 以上版本也有大量出現,所以咱們認爲可能也有其餘緣由致使此類問題:

▍IO 負載太高

許多類的 finalize() 都須要釋放 IO 資源,當 APP 打開的文件數目過多,或者在多進程或多線程併發讀取磁盤的狀況下,隨着併發數的增長,磁盤 IO 效率將大大降低,致使 finalize() 方法中的 IO 操做運行緩慢致使超時。

▍FinalizerDaemon 中線程優先級太低

FinalizerDaemon 中運行的線程是一個守護線程,該線程優先級通常爲默認級別 (nice=0),其餘高優先級線程得到了更多的 CPU 時間,在一些極端狀況下高優先級線程搶佔了大部分 CPU 時間,FinalizerDaemon 線程只能在 CPU 空閒時運行,這種狀況也可能會致使超時狀況的發生,(從 Android 8.0 版本開始,FinalizerDaemon 中守護線程優先級已經被提升,此類問題已經大幅減小)

▍解決方案

當問題出現後,咱們應該找到問題的根本緣由,從根源上去解決。然而對於這個問題來講卻不太容易實現,和其餘問題不一樣,這類問題緣由比較複雜,有系統緣由,也有 APP 自身的緣由,比較難以定位,也難以系統性解決。

▍理想措施

理論上咱們能夠作的措施有:

  • 1. 減小對 finalize() 方法的依賴,儘可能不依靠 finalize() 方法釋放資源,手動處理資源釋放邏輯。

  • 2. 減小 finalizable 對象個數,即減小有 finalize() 方法的對象建立,下降 finalizable 對象 GC 次數。

  • 3.finalize() 方法內儘可能減小耗時以及線程同步時間。

  • 4. 減小高優先級線程的建立和使用,下降高優先級線程的 CPU 使用率。

▍止損措施

理想狀況下的措施,能夠從根本上解決此類問題,但現實狀況下卻不太容易徹底作到,對一些大型APP來講更難以完全解決。那麼在解決問題的過程當中,有沒有別的辦法可以緩解或止損呢?總結了技術網站上現有的方案後,能夠總結爲如下幾種:

· 手動修改 finalize() 方法超時時間

理論上咱們能夠作的措施有:

  • 1. 減小對 finalize() 方法的依賴,儘可能不依靠 finalize() 方法釋放資源,手動處理資源釋放邏輯。

  • 2. 減小 finalizable 對象個數,即減小有 finalize() 方法的對象建立,下降 finalizable 對象 GC 次數。

  • 3.finalize() 方法內儘可能減小耗時以及線程同步時間。

  • 4. 減小高優先級線程的建立和使用,下降高優先級線程的 CPU 使用率。

    ▍止損措施

    理想狀況下的措施,能夠從根本上解決此類問題,但現實狀況下卻不太容易徹底作到,對一些大型APP來講更難以完全解決。那麼在解決問題的過程當中,有沒有別的辦法可以緩解或止損呢?總結了技術網站上現有的方案後,能夠總結爲如下幾種:

    · 手動修改 finalize() 方法超時時間


    詳情請見∞

    這種方案思路是有效的,可是這種方法倒是無效的。Daemons 類中 的 MAX_FINALIZE_NANOS 是個 long 型的靜態常量,代碼中出現的 MAX_FINALIZE_NANOS 字段在編譯期就會被編譯器替換成常量,所以運行期修改是不起做用的。MAX_FINALIZE_NANOS默認值是 10s,國內廠商經常會修改這個值,通常有 15s,30s,60s,120s,咱們能夠推測廠商修改這個值也是爲了加大超時的闕值,從而緩解此類 Crash。

    · 手動停掉 FinalizerWatchdogDaemon 線程


    詳情請見∞

    這種方案利用反射 FinalizerWatchdogDaemonstop() 方法,以使 FinalizerWatchdogDaemon 計時器功能永遠中止。當 finalize() 方法出現超時, FinalizerWatchdogDaemon 由於已經中止而不會拋出異常。這種方案也存在明顯的缺點:

    • 1. 在 Android 5.1 版本如下系統中,當 FinalizerDaemon 正在執行對象的 finalize() 方法時,調用 FinalizerWatchdogDaemonstop() 方法,將致使 run() 方法正常邏輯被打斷,錯誤判斷爲 finalize() 超時,直接拋出 TimeoutException。

    • 2. Android 9.0 版本開始限制 Private API 調用,不能再使用反射調用 Daemons 以及 FinalizerWatchdogDaemon 類方法。

    ▍終極方案

    這些方案都是阻止 FinalizerWatchdogDaemon 的正常運行,避免出現 Crash,從原理上仍是具備可行性的:finalize() 方法雖然超時,可是當 CPU 資源充裕時,FinalizerDaemon 線程仍是能夠得到充足的 CPU 時間,從而得到了繼續運行的機會,最大可能的延長了 APP 的存活時間。可是這些方案或多或少都是有缺陷的,那麼有其餘更好的辦法嗎?

    What should we do? We just ignore it.

    咱們的方案就是忽略這個 Crash,那麼怎麼可以忽略這個 Crash 呢?首先咱們梳理一下這個 Crash 的出現過程:

    • 1. FinalizerDaemon 執行對象 finalize() 超時。

    • 2. FinalizerWatchdogDaemon 檢測到超時後,構造異常交給 Thread 的 defaultUncaughtExceptionHandler 調用 uncaughtException() 方法處理。

    • 3. APP 中止運行。

    Thread 類的 defaultUncaughtExceptionHandler 咱們很熟悉了,Java Crash 捕獲通常都是經過設置 Thread.setDefaultUncaughtExceptionHandler() 方法設置一個自定義的 UncaughtExceptionHandler ,處理異常後經過鏈式調用,最後交給系統默認的 UncaughtExceptionHandler 去處理,在 Android 中默認的 UncaughtExceptionHandler 邏輯以下:


    從系統默認的 UncaughtExceptionHandler 中能夠看出,APP Crash 時彈出的中止運行對話框以及退出進程操做都是在這裏處理中處理的,那麼只要不讓這個代碼繼續執行就能夠阻止 APP 中止運行了。基於這個思路能夠將這個方案表示爲以下的代碼:


· 可行性

這種方案在 FinalizerWatchdogDaemon 出現 TimeoutException 時主動忽略這個異常,阻斷 UncaughtExceptionHandler 鏈式調用,使系統默認的 UncaughtExceptionHandler 不會被調用,APP 就不會中止運行而繼續存活下去。因爲這個過程用戶無感知,對用戶無明顯影響,能夠最大限度的減小對用戶的影響。

· 優勢

  • 1. 對系統侵入性小,不中斷 FinalizerWatchdogDaemon 的運行。

  • 2.Thread.setDefaultUncaughtExceptionHandler() 方法是公開方法,兼容性比較好,能夠適配目前全部 Android 版本。

總結

無論什麼樣的緩解措施,都是治標不治本,沒有從根源上解決。對於這類問題來講,雖然人爲阻止了 Crash,避免了 APP 中止,APP 可以繼續運行,可是 finalize() 超時仍是客觀存在的,若是 finalize() 一直超時的情況得不到緩解,將會致使 FinalizerDaemonFinalizerReference 隊列不斷增加,最終出現 OOM 。所以還須要從一點一滴作起,優化代碼結構,培養良好的代碼習慣,從而完全解決這個問題。固然 BUG 不斷,優化不止,在解決問題的路上,緩解止損措施也是很是重要的手段。誰能說能抓老鼠的白貓不是好貓呢?




                                                                                江 義 旺
                                                              滴滴 | 業務平臺技術
                                                              資深軟件研發工程師

曾就任於奇虎360,長期從事移動端研發,2018年加入滴滴,專一於安卓移動端性能優化,架構演進,新技術探索,開源項目DroidAssist 做者。



                                                            看完江義旺同窗的分享
                                                            你有怎樣的心得體會?
                                                            請在留言區告訴咱們吧


相關文章
相關標籤/搜索