突然彈出王者榮耀停止運(yùn)行,GC超時導(dǎo)致的后臺應(yīng)用崩潰問題分析


寫在前面
這個問題之所以會拿出來仔細(xì)分析,一方面是因?yàn)檫@個問題不是簡單的應(yīng)用崩潰而是框架層的報錯,另一方面是因?yàn)橄Mㄟ^這個問題梳理下后臺GC的超時檢測機(jī)制怎樣的,這樣我們后面在應(yīng)用層如果重寫finalize
方法回收時會考慮的更加全面點(diǎn)。
問題背景
復(fù)現(xiàn)概率: 偶現(xiàn)
問題版本:?Android R
問題現(xiàn)象: 處于微信界面,突然彈出王者榮耀停止運(yùn)行
初步分析
拿到問題日志后,先看下報錯的堆棧

單單從這段堆??吹脑挘?code>BulkCursorToCursorAdaptor執(zhí)行finalize
超過了10s,導(dǎo)致FinalizerWatchdogDaemon
報錯,FinalizerWatchdogDaemon
字面上看像是監(jiān)測回收超時的守護(hù)線程。
看下FinalizerWatchdogDaemon
代碼中的作用解釋

簡單解釋下就是:如果對象的finalize
出現(xiàn)阻塞超時了會導(dǎo)致進(jìn)程退出
這個問題中對應(yīng)的是數(shù)據(jù)庫的關(guān)閉,當(dāng)然也可以發(fā)生在其它場景下,只要重寫了成員函數(shù)finalize
的對象都有可能會遇到這個問題,所以如果再遇到GC超時的報錯,報錯堆棧AndroidRuntime:at java.lang.Daemons$
上面的內(nèi)容可能會不一樣。
那么對于重寫了成員函數(shù)finalize
的對象,當(dāng)它們被GC決定要被回收時,會立刻回收嗎?
其實(shí)不會馬上被回收,而是被放入到一個隊列中,等待FinalizerDaemon
守護(hù)線程去調(diào)用它們的成員函數(shù)finalize
后再被回收。

超時閾值

注釋中對于該值的說明是它很快將被移除,實(shí)際這個值在代碼中并沒有起到真正的作用了,更新它的值是為了方便在外邊讀取到。
真正的超時閾值是通過VMRuntime.getFinalizerTimeoutMs
獲取,默認(rèn)值是10s.

超時檢測
通過watchdog機(jī)制檢測finalizer
在超時時間內(nèi)有沒有成功析構(gòu)回收對象

Step1 GC前的檢查

開啟回收之前,needToWork
會被置為true,此時sleepUntilNeeded
返回的是true,所以線程不會wait

如果此時線程處于wait
,被中斷了或者有OOME
發(fā)生時,這個時候回到開頭判斷下isRunning()
,也就是看下回收對象這個線程是否為空,如果該線程為空的話,這個循環(huán)體就沒有必要再繼續(xù)執(zhí)行下去了。

Step2 等待GC完成
這一步是等待回收結(jié)束的過程,這個睡眠過程中如果被中斷,說明在這個周期內(nèi)完成了析構(gòu),直接返回null

sleepForNanos
對應(yīng)的函數(shù)很簡單,如果在超時時間內(nèi)完成GC,就會計算傳進(jìn)來的超時閾值減去當(dāng)前已經(jīng)睡眠的時間,如果這個差值小于0,說明睡眠的時間超過了閾值。

Step3 GC處理超時
如果第二步中的超時時間內(nèi)析構(gòu)沒有完成,則返回析構(gòu)的對象,觸發(fā)finalizerTimedOut
。
到了這一步是最不希望看到的結(jié)局,此時系統(tǒng)會彈出應(yīng)用停止運(yùn)行的報錯框。
注意這個時候并沒有立刻殺死進(jìn)程,殺死進(jìn)程的選擇權(quán)交給了用戶,即通過彈窗展示給用戶,但對于用戶來說會一頭霧水

分析結(jié)論
這種問題其實(shí)還是比較常見的,特別是低內(nèi)存的機(jī)器上。RootCasue
就是對象回收超時了,一般是由于隊列中等待FinalizerDaemon
線程回收的對象太多導(dǎo)致,或者此時系統(tǒng)資源異常緊張比如CPU負(fù)載過高或者低內(nèi)存環(huán)境下。
場景實(shí)測
模擬還原現(xiàn)場
通過模擬GC
時耗時操作,應(yīng)用退到后臺后10s會彈出報錯框,堆棧如下

驗(yàn)證了超時時間的確是10s,同時也驗(yàn)證了GC時耗時的操作確實(shí)會可能觸發(fā)這個現(xiàn)象
對比機(jī)情況
在手頭的小米note9 pro
上進(jìn)行場景模擬測試,模擬GC耗時100s的情況

在小米的機(jī)器上,到了默認(rèn)的10s后并不會有彈窗,說明小米肯定修改了超時時間,第一次是等待了全部的100s后竟然正?;厥?,說明超時時間設(shè)置的比較大。緊接著下一次在達(dá)到了近80s時,進(jìn)程收到signal 9
直接被kill了,此時再點(diǎn)擊應(yīng)用是冷啟動。
小米修改了超時閾值(超過100s),通過直接sig 9殺掉了進(jìn)程,沒有報錯彈窗,所以用戶無感知
測試機(jī)情況
同樣的在我們的機(jī)器上模擬GC耗時100s的情況
退出應(yīng)用到后臺,此時系統(tǒng)觸發(fā)GC回收,達(dá)到十秒鐘時,界面上直接彈出停止運(yùn)行的報錯框,此時只有點(diǎn)擊了關(guān)閉應(yīng)用,才會去kill進(jìn)程
修改策略
在GC規(guī)定的超時時間內(nèi)如果沒有完成析構(gòu),直接sig 9
給對應(yīng)進(jìn)程