天天看點

再談Finalizer對象--大型App中記憶體與性能的隐性殺手

    在上一篇《提升Android下記憶體的使用意識和排查能力》的文章中,多次提到了Finalizer對象。也可以看到該對象的清理至少是需要兩次GC才能完成,而在Android5.0,尤其是6.0以後的系統中,對于該對象的回收變得更加的慢。我們在開發的時候往往關注記憶體的配置設定、洩漏,卻容易忽視Finalizer對象,其實在大型App中,該對象是引起記憶體和性能問題的一個不可忽視的元兇。在類似于雙十一會場的界面中,在使用一段時間後,裝置會變得越來越慢,記憶體使用量也不斷攀升,甚至容易引發OOM,這個有一個重要原因就和Finalizer對象的過度使用有關。為什麼過度的使用Finalizer對象會對性能和記憶體都造成危害呢?我們不妨來看下Finalizer對象的原理。

    Finalizer對象是指Java類中重寫了finalize方法,且該方法不為空的對象。當運作時環境遇到建立Finalizer對象的時候,既建立對象執行個體的時候,會先判斷該對象是否是Finalizer對象,如果是,那麼在構造函數過程中會把生成的對象再封裝成Finalizer對象并添加到 Finalizer連結清單中。在運作時環境中,也會有一個專門的FinalizerReference來處理和Finalizer對象的關聯。我們可以看一下Android 7.0上的FinalizerReference的代碼:

    通過斷點,我們也可以還原對象的建立過程,例如:

再談Finalizer對象--大型App中記憶體與性能的隐性殺手
再談Finalizer對象--大型App中記憶體與性能的隐性殺手

    通過斷點,我們也可以清晰的看到,在上面兩個對象的建立過程中,都進入了FinalizerReference的add函數。在該函數中,又會增加一個包裝的對象FinalizerReference,這本身就是對記憶體的一個開銷。另外,從上面的代碼,我們很容易看到一個問題,在add和remove的時候,都會遇到synchronized (LIST_LOCK)的同步鎖問題。當大量的這種類型的對象需要同時建立或者回收的時候,就會遇到線程間的鎖開銷問題。在一個大型app中,這是不得不考慮的因素。而在Android4.2之前,同步對象用的是class本身,也就是鎖的粒度會更大,當系統中有不止一個FinalizerReference對象的時候性能開銷會更大。另外,在添加對象的時候,在隊列中也會遇到另外一個鎖,下面代碼中會分析到。

    在Android系統中,會有一個專門的線程來實作該對象的回收。我們在檢視線程的時候就可以看到有這樣一個FinalizerDaemon線程。

首先先看下該線程的代碼:

    通過代碼,我們可以看到,在程序起來後,會啟動一個FinalizerDaemon線程和該線程的守護線程。在前面的代碼中我們可以看到,在Finalizer對象add的時候,會關聯到一個ReferenceQueue的queue中。在該線程進行處理這些對象的時候,首先會從ReferenceQueue的隊列中擷取連結清單的頭結點。我看可以看下poll方法的代碼:

    從這裡我們可以看到,這裡會遇到另外一個鎖lock, 該鎖和FinalizerReference代碼中的鎖是獨立的。我們可以看到,在doFinalize函數中,會首先調用FinalizerReference對象的remove方法,該方法前面已經可以看到存在在同步鎖。也就是在加入和删除Finalizer對象的時候會同時遇到這兩個鎖開銷。

    在doFinalize函數中,我們可以看到,對該對象的finalize方法的調用。這裡看似沒有問題,但是一旦該對象的finalize寫法有問題:耗時、進入其他資源、不斷抛出異常等待等等就會遇到問題。這些都會引起本身該代碼的性能問題,更進一步會影響到整個App中的Finalizer對象的記憶體回收,一旦記憶體回收不過來,系統就會引發崩潰。

    在系統中還有一個FinalizerWatchdogDaemon的守護程序,該程序會監控FinalizerDaemon線程的運作,一旦FinalizerDaemon在處理一個對象的時候超過10s中,那麼就會結束程序,導緻崩潰。我們可以檢視FinalizerWatchdogDaemon的主要代碼:

     因為finalize方法調用的不确定性,是以不僅僅會導緻性能問題,還會引起記憶體問題和穩定性問題。

     我們通過代碼來模拟一下寫法不準确帶來的危害。

     在點選按鈕的時候會建立1000個View,而每個view在回收的時候都需要等待1s的時間。當連續點選按鈕的時候,我們可以看到記憶體會不斷的往上增加,而基本不會減少。

再談Finalizer對象--大型App中記憶體與性能的隐性殺手

    通過線程的堆棧資訊,我們也可以觀察者兩個線程正在做的事情:

再談Finalizer對象--大型App中記憶體與性能的隐性殺手
再談Finalizer對象--大型App中記憶體與性能的隐性殺手

     在這種情況下,線程都還在幹活,沒有到達崩潰的程度。但是記憶體的回收已經變得極其緩慢,及時手動觸發GC,也無濟于事,對象已經非常的多:

再談Finalizer對象--大型App中記憶體與性能的隐性殺手

    如果這個時候再繼續點選按鈕,一旦記憶體回收遇到問題,就會引發崩潰,如下所示,引發了JNI ERROR (app bug): weak global reference table overflow (max=51200)的崩潰,因為weak reference對象太多,已經超過極限:

再談Finalizer對象--大型App中記憶體與性能的隐性殺手

   我們再來模拟另外一種情況,finalize函數長時間無法傳回的情況。代碼如下:

     在index值為10000的時候,finalize函數需要20s的執行時間,那麼記憶體和最後的穩定性情況會怎麼樣呢?

再談Finalizer對象--大型App中記憶體與性能的隐性殺手

    記憶體會和我們預期的一緻,在前面幾次點選的時候,由于finalize函數執行順利,我們可以看到GC過程,記憶體沒有快速上升。但是到了10次以後,記憶體就開始不斷攀升。這個時候,我們讓App靜默等待,結果10s多後,就發生了逾時崩潰,如下所示:

再談Finalizer對象--大型App中記憶體與性能的隐性殺手

    由于在一些裝置上UI和Render線程的Nice優先級值都是負數,而該線程的Nice值一般情況下是0,也就是預設值。在UI等其他線程都繁忙的時候,finalize的回收并不會很快,這樣就會導緻記憶體回收變慢,進一步影響到整體的性能。特别是很多低性能的裝置,更加容易暴露這方面的問題。

    之前的檔案已經介紹過,從Android 5.0開始,每個View都包含了一個或者多個的Finalizer對象,RenderNode對象的增加會導緻一定的記憶體和性能問題,尤其是當一個界面需要建立大量的控件的時候,該問題就會特别明顯,例如在手淘中的某些Weex頁面,由于渲染界面的樣式是過前端控制的,沒有分頁的概念,這樣一次性建立非常多的控件,并且很多控件都額外使用了其他Finalizer對象,這樣就會導緻這種情況下,記憶體會正常很快,在低端裝置上,有可能就會來不及回收而引起性能和穩定性問題。我們可以看下View和RenderNode的代碼:

     當然,除了View以外,Path,NinePatch,Matrix,檔案操作的類,正規表達式等等都會建立Finalizer對象,在大型App中過多的使用這些操作對記憶體和性能和穩定性都會帶來比較大的影響。

    如果大量的Finalizer對象累積無法及時回收,那麼我們可以預見到,FinalizerDaemon線程就會增加越來越重的負擔,在GC過程中,需要檢測的對象越來越多,所占用的CPU資源也必然增加。整體CPU占用過多,肯定也會對UI線程和業務線程産生幹擾,對性能産生影響,而且由于其占用的記憶體無法及時釋放,那麼整個記憶體的使用率和配置設定過程也會對性能造成影響。另外考慮到同步鎖的影響,線上程越多的情況下,在建立Finalizer對象的過程中,也會影響到使用方的線程的性能。

    在手淘的性能體系中,有專門對Finalizer對象做了監控。在接入OnLineMonitor較新版本的App中都可以監控到Finalizer的數量和分布(統計分布的功能需要額外開啟)。例如,我們啟動手淘,點選微淘,問大家,天貓,天貓國際這幾個界面,在最後的報告中,我們就可以看到這些界面的Finalizer變化,如下圖所示(Nexus 6p裝置上):

再談Finalizer對象--大型App中記憶體與性能的隐性殺手

    我們可以看到,從首頁開始,Finalizer對象一直在增加,因為這幾個界面都沒有銷毀。而到了【天貓】界面,增加的很快。我們再來看下這些界面的主要Finalizer對象分布:

再談Finalizer對象--大型App中記憶體與性能的隐性殺手
再談Finalizer對象--大型App中記憶體與性能的隐性殺手

    上圖我們可以看到Finalizer對象分布情況,在回到首頁然後進入天貓之後,RenderNode和Matrix對象有了明顯的上升。這與控件增加較多以及很多控件的圖檔使用了圖檔效果有關。上面的檢測是在Nexus 6p裝置上,在該裝置上Finalize線程的回收還算比較及時。一旦包含大量的Finalizer對象的界面很多,在性能較差的裝置上就會導緻Finalizer對象的累積,影響到記憶體和性能,在部分極端的裝置上還會引發崩潰的問題。

    除了本地報表有監控外,在背景我們也進行了整體的Finalizer對象的跟蹤,能夠跟蹤各個界面的Finalizer對象數量,後續可以對Finalizer過高的界面進行有針對性的優化,以加快記憶體的回收,提升整體的性能。

    在記憶體的使用上,除了前面提到的熟悉記憶體工具和提高意識外。在我們寫代碼的時候,也要加強Finalizer對象的了解和警覺,了解哪些系統類是有Finalizer對象,并了解Finalizer對記憶體,性能和穩定性所帶來的影響。特别是我們自己寫類的時候,要盡量避免重寫finalize方法,即使重寫了也要注意該方法的實作,不要有耗時操作,也盡量不要抛出異常等。隻有這樣才能寫出更加優秀的代碼,才能在手淘這種超級App中運作的更加流暢和穩定。

繼續閱讀