記憶體問題一直是大型App的開發人員比較頭痛的問題,特别是像手淘這種超級的App,App中到處都是帶有圖檔和視訊的界面,而且這些功能都是由不同的團隊甚至不同的事業部開發的,要整體上去管控和排查記憶體的問題變得相當的複雜。之前,我們多個線上版本都存在着嚴重的Activity等記憶體洩漏和不合理記憶體使用。這不是偶然,一個很重要的原因就是我們很多的開發測試人員側重業務開發,忽略記憶體和性能,而且沒有站在全局性的角度去考慮資源的使用。認為我自己的子產品多緩存一些就會加快速度,以空間換時間看似正确,但是在手淘這樣的超級App中是不可取的,需要嚴格限制,否則不要說裡面幾百個子產品,有幾十個子產品這樣來做,其結果都會是災難性的,不但沒有加快速度,反而會拖慢速度以及帶來很多穩定性問題。
經過一年多的更新,現在的Android Studio所帶的工具已經相當的成熟。以前我們還停留在使用MAT來分析記憶體,但是現在Android Studio的記憶體分析工具已經相當的強大,已經完全可以抛開MAT來實作更為直覺的記憶體檢測。我想,作為一個大型App的開發人員和測試人員,掌握這些技能都是必不可少的,也是提高整個App品質的關鍵所在。
在使用工具分析記憶體之前,我們需要了解一下記憶體回收上的一些政策,否則很多時候排查到的可能都不是真正的問題。
我們知道,Java的記憶體回收如果有強引用存在,那麼這個對象的記憶體不會回收。那麼這個對象的引用如果不存在,是不是這塊記憶體就會回收呢?答案是否定的,VM有自己的回收政策,不會因為一個對象的引用為空了就立馬對它進行回收。也就是說,回收政策需要達到一定的觸發門檻值。我們可以看一個Demo,寫如下的配置設定對象的方法:
在記憶體充足的情況下,我們點選按鈕4次,執行了4遍該函數,這個時候可以看到堆記憶體呈現了4次增長。如下圖所示:

靜置在那半個小時,記憶體仍然會維持現狀,VM并不會來執行實際的GC。我們可以Dump記憶體看看記憶體中的對象:
我們可以看到,4000個ImageView對象仍然毫發無損的在記憶體中殘留,系統沒有回收其記憶體。不管是Dalvik還是ART環境,包括最新的Android N,都可能出現這樣的情況,具體是否每次都保持(等量)增長等還要看手機記憶體剩餘情況和手機的GC政策。是以,我們在檢測記憶體占用或者記憶體洩漏之前,一定要執行工具自帶的GC觸發功能,否則結果會不準确。明明沒有洩漏或者沒有占用,而Dump出來的堆中提示占用很大或者洩漏。
通過Memory Monitor工具,我們可以看到其引用的情況。點選其中一個ImageView,我們看下它的引用情況:
可以看到,其引用路徑是被一個FinalizerReference所持有。該對象已經标記為紅色,表明我們自己的代碼中已經沒有其他引用持有該對象,狀态是等待被回收。
通過手動觸發GC,我們可以來主動回收這塊記憶體,點選如下圖所示的按鈕,觸發GC。
當然,這裡還有一個問題存在,因為剛才建立的屬于Finalizer對象,該對象前面的文章已經分析過,需要至少兩次GC才能真正回收其記憶體。是以,第一次觸發GC的時候,我們可以看一下記憶體的變化。
第一次觸發GC後,記憶體隻是部分的下降,這個時候Finalize連結清單中的對象被回收,但是ImageView還沒有回收(在不同Rom和Android 版本下可能會存在少許差異,在Dalvik下的差異會更大一些)。我們可以看一下堆記憶體:
該對象的引用鍊路全部标記為紅色,已經沒有強引用指向該對象,剛才的FinalizerReference已經執行了清理。但是第一次GC隻是執行了Finalizer清理,而沒有真正的回收這部分對象。是以還需要再一次的觸發GC,再次執行GC後,我們可以看到堆記憶體下降,這些對象被回收了。
我們可以通過Dump堆記憶體來驗證是否已經回收,如下圖所示,對記憶體中已經沒有了剛才的ImageView對象,确實已經被回收了。
通過前面的分析我們可以知道,并不是引用釋放了,記憶體就會回收。在實際使用手機淘寶的過程中,我們也可以觀察堆記憶體的變化,在退出一個界面的時候很可能是不會有GC的,堆記憶體不會變化。如果這個時候用MAT工具去Dump記憶體,那結果很多是不夠正确的。而如果後續再做其他操作,引發GC後,才會使得結果更加準确,當然我們可以手動的去觸發GC。
在前面的文章中已經提到過,代碼調用GC,隻是告訴VM需要GC,但是是否真正的去執行GC還是要看VM的配置和是否達到門檻值。前面也提到,在執行手動GC的時候,Dalvik和ART下會有比較明顯的差異。Android 5.0開始增加了更多的GC方式,到了6.0,7.0在GC方面有了更多的優化。特别是在執行Finalizer對象方面,Android 5.0開始回收就沒有那麼快,單次執行GC,并不會導緻失去引用的Finalizer對象進行完全回收,如果要更好的回收Finalizer對象,需要執行System.runFinalization()方法來強制回收Finalizer對象。
我們将測試代碼後面加上主動調用GC的代碼,如下:
在不同的Android版本上看下執行效果,點選按鈕執行多次。在Android 4.4.4版本的裝置上,記憶體基本已經回收,如果将1000次修改為10次,偶爾可以看到有ImageView已經沒有引用存在,但是仍然沒有回收。如下圖所示:
從這裡可以看出來,在該版本下,大部分的對象在沒有強引用後,調用System.gc()就會被回收。我們再看下ART上的情況,在ART下,卻發生了不一緻的表現。在調用後沒有進行GC。檢視堆記憶體,我們也可以看到,4000個ImageView仍然存在,并未執行GC。
看來在ART後,這部分的GC政策做了調整,System.gc()沒有記憶體回收。我們可以看下源碼,在Android 4.4.4下的源碼:
我們可以看到,在調用gc函數後,直接調用了運作時的gc。再來看下5.0上的代碼:
從源碼我們可以發現,System.gc()函數,已經有了變化,從5.0開始,多了一些判斷條件。是否執行gc,是依賴于justRanFinalization變量,而變量justRanFinalization在runFinalization後才會變為true。也就是說,直接調用System.gc()方法并沒有調用Runtime.getRuntime().gc(),隻是做了一個标記将runGC變量設定為true。
在runFinalization函數中,如果标記了runGC的,會先執行一次gc,然後清理Finalizer對象,并标記為依據清理過了Finalizer對象。這樣在下次gc調用的時候,就會真正執行一次gc,以回收Finalizer對象。
在Android ART的裝置上,我們将調用gc的方式做下變更:
這裡直接調用了Runtime.getRuntime().gc()。這個時候記憶體回收确實會有變化。但是Finalizer對象仍然可能存在,在Android 5.0的時候會回收一部分,但是從6.0開始,單次調用gc,Finalizer對象卻不一定回收。如下圖所示,雖然所有引用鍊路已經不複存在,但是記憶體仍然沒有回收:
那麼執行2次gc是否就都能把記憶體回收了呢?我們修改下代碼:
這裡連續2次調用了gc,按理是可以回收Finalizer對象的,但是由于兩次調用gc的間隔太短,而Finalizer對象是由專門的線程執行回收的,是以也不一定能完全回收。這個和線程的排程情況有關系。例如執行上面代碼可能出現的結果是部分回收:
如果想要全部回收,可以在中間停頓一些間隔,或者增加System.runFinalization()方法的調用。這樣就能将目前可以回收的記憶體基本都回收了。我們在Android Studio的觸發GC的按鈕,也是通過BinderInternal$GcWatcher等代碼來執行記憶體回收的。當然,在實際的業務代碼中,不要主動調用gc,這樣可能會導緻額外的記憶體回收開銷,在檢測代碼中,如果需要檢測記憶體,最好按照gc,runFinalization,gc的順序去回收記憶體後,再做記憶體的Dump分析。
Memory Monitor工具比起MAT一個很大的優勢就是可以非常簡單的檢視記憶體占用,而且可以迅速找到自己的子產品所占用的堆記憶體,是以希望開發和測試人員都能夠遷移到Android Studio所帶的工具上來。如何檢視記憶體占用?真的是非常的簡單,而且可以找到很細小的記憶體占用情況。
例如,這裡自定義一個類,然後建立該類,代碼如下:
這裡隻是建立了一個該對象,但是也很容易可以跟蹤到該對象的記憶體情況。通過Memory Monitor的【Dump Java Heap】按鈕可以把目前堆記憶體顯示出來,如下圖所示:
這裡是預設檢視方式,我們可以切換到以包名的形式檢視。這樣就可以很容易的找到我們自己的代碼了。如下所示:
切換檢視方式後,我們可以很容易的就找到自己所寫的代碼記憶體占用情況。這裡可以看到執行個體的個數,占用記憶體的大小等情況。
例如在打開手機淘寶,簡單操作一會後再來觀察記憶體占用情況。按照包名檢視後,我們很容易就可以看到整個堆記憶體的占用情況,如下圖所示:
通過上圖我們很容易看到,在com.taobao包名下占用了近240多M的記憶體。繼續往下看,聚劃算子產品,圖檔庫子產品占了大頭。點選ju子產品,展開後,又可以看到該包名下的記憶體占用:
通過上圖我們可以清晰的看到,在ju包名下的記憶體分布情況。
是以,在記憶體占用的檢查上,Android Studio已經給我們提供了強大的工具,各個開發和測試人員已經可以很簡單的檢視到自己開發的子產品占據的記憶體大小,然後對記憶體的使用做一些改進等措施。這裡也可以通過右側的Instance視窗檢查各個執行個體的引用以及排查不合理的記憶體強制占用。
Memory Dump下來後,我們可以檢查Java堆的Activity記憶體洩漏和重複的String。很多人還習慣于MAT分析工具,其實Memory Monitor已經包含了這個功能,而且使用非常簡單。
首先dump記憶體,如前面分析的那樣,在右側可以看到【Analyzer Tasks】按鈕。
然後,我們點選該按鈕,就可以看到分析洩漏的工具。
這裡可以隻勾選 檢測洩漏Activity選項,然後選擇執行。這樣就可以看到洩漏的Activity了。
通過引用指向,我們可以比較容易的判斷出不該持有的強引用關系,而且該工具從上到下的排序,已經做了初步的判斷。
當然在檢測洩漏和占用之前,需要點選2次GC的按鈕,這樣結果才會相對準确。
對于Android Studio提供的記憶體分析工具,使用起來非常簡單,會比Mat工具要快捷,排查問題也更加容易。是以Android的開發和測試人員,應該盡可能的都遷移到該工具上來,并能夠熟練的掌握記憶體分析工具,這樣才能讓自己開發的子產品品質更加的優秀。
以上主要是針對Android Studio 2中的使用方式,在今年的Android Studio 3 Preview版本中,記憶體這塊的分析工具更加強大,可以在面闆上直接看到更細粒度的記憶體占用,不僅僅是Java的對記憶體了。
對于需要更細粒度和更全面的分析一些記憶體的細節,本文所涉及的記憶體知識還是不夠的,還需要了解Linux下的記憶體機制以及Android下的一些記憶體機制,例如按頁配置設定,共享記憶體,GPU記憶體等等。