前言
手把手講解系列文章,是我寫給各位看官,也是寫給我自己的。
文章可能過分詳細,但是這是為了幫助到儘量多的人,畢竟工作5,6年,不能老吸血,也到了回饋開源的時候.
這個系列的文章:
1、用通俗易懂的講解方式,講解一門技術的實用價值
2、詳細書寫原始碼的追蹤,原始碼截圖,繪製類的結構圖,儘量詳細地解釋原理的探索過程
3、提供Github 的 可執行的Demo工程,但是我所提供程式碼,更多是提供思路,拋磚引玉,請酌情cv
4、集合整理原理探索過程中的一些坑,或者demo的執行過程中的注意事項
5、用gif圖,最直觀地展示demo執行效果
如果覺得細節太細,直接跳過看結論即可。
學到老活到老,路漫漫其修遠兮。與眾君共勉 !
正文大綱jvm記憶體管理常識檢測以及處理記憶體抖動檢測以及處理記憶體洩漏正文vm記憶體管理常識LMK (LowMemoryKill)機制android底層會在系統記憶體告急的時候,按照一定規則殺死一些程序來滿足其他程序的記憶體需要。其中 消耗記憶體的高低就是其中一項指標,所以,優化app的記憶體佔用,能夠有效降低app被系統殺死的概率。GC STW機制GC,垃圾回收程序,在GC執行緒執行任務的時候,會存在一個 STW (stop the world) 機制,他就會把其他所有執行緒都掛起。如果GC非常頻繁地呼叫,那就會導致主執行緒不流暢,給使用者的感覺就是卡頓。記憶體抖動頻繁引起OOM記憶體抖動太頻繁,導致大量物件頻繁建立和銷燬,會產生大量不連續的記憶體空間,如果此時有一個大物件需要申請記憶體,就有可能申請失敗,導致OOM記憶體溢位。一句話解釋 記憶體洩漏長生命週期的物件持有短生命週期物件的強引用,在短生命週期物件需要回收的時候發現不能被回收,視為洩漏GC回收 可達性分析GC執行緒判定 一個物件是不是可以回收,是根據可達性分析演算法,計算GcRoot,從GcRoot向下搜尋,把GcRoot沒有直接關聯的物件全部作為垃圾來回收。強軟弱虛四大引用強和虛自不必說。強 最常見,沒有特殊處理的都是強引用(包括,匿名內部類會持有外部類的強引用)。虛引用沒什麼用,不予討論。軟引用,用來定義一些還有用,但是不是必須的物件,使用SoftRefrence<T>修飾,在記憶體緊張的時候,GC回收之後,使用SoftRefrence<T>修飾,如果系統還有足夠的記憶體可用,那麼軟引用關聯的物件就不會被回收。如果不足,則回收軟引用關聯的物件。弱引用(WeakRefrence<T>),比軟引用更弱一些,只要GC觸發,弱引用關聯的物件就會被回收。注意,使用軟和弱引用,要判定關聯物件是否為空。檢測以及處理記憶體抖動我們使用s開發,平時我們執行app,一般會點 RunApp,但是還有另一個選擇, 那就是 profileApp, 執行app起來之後,會在as下方看到profile 視窗
如果記憶體的圖中抖動得非常明顯,比如像這樣的心電圖一樣:
那就說明非常明視訊記憶體在記憶體抖動,急需處理:
這裡有個坑:
如果你從圖形中觀察到,記憶體走勢平穩,並沒有出現上滿模擬抖動的圖中那麼誇張,是不是就不存在記憶體抖動呢?並不是。因為我們的gc,是在記憶體不可用的情況下才會去回收記憶體,如果app佔用記憶體一直比較少,沒有觸及gc的臨界值,那麼就不會出現斷崖式下跌. 那麼這樣就觀察不出記憶體抖動了,怎麼辦呢?
解決方法
點選它,一段時間之後,再點一下:你就能在下方發現一張表格:[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-XZh3ddJV-1571377551222)
到此為止,就找到了建立物件的元凶,以這個為線索,找到你們自己包名下的類和方法,確定是我們自己的程式碼在不合理地建立物件.
再往後,就是根據各自的業務程式碼去做優化了,記住一個宗旨:不要讓程式碼幹多餘的事。如果是我們呼叫了系統的api導致了不合理地大量物件的建立,那麼就要考慮這個系統API為什麼會這樣建立物件,有沒有其他方法避免嗎,從業務程式碼層來合理使用這個api,實在不行再考慮自定義api或者換個系統api。
在我們做了一次優化之後,再profile執行一次app,再重複上面的過程。以此類推,直到記憶體抖動達到理想狀態。
總結優化記憶體抖動,核心就是防止頻繁建立物件。常見的反面教材就是:迴圈中建立物件,大量呼叫的api中建立物件。而優化的主要手段,就是物件複用,常見的手段是:物件池,像是 Handler的Message 單鏈表池,Glide的bitmap池等。
檢測以及處理記憶體洩漏經典案例:處理handler非同步任務導致的記憶體洩漏方法
在Activity的onDestroy中移除所有的任務 @Override protected void onDestroy() { super.onDestroy(); handler.removeCallbacksAndMessages(null);//移除所有任務 }使用靜態內部類 + Activity弱引用的方式 MyHandler handler = new MyHandler(this); private static class MyHandler extends Handler { WeakReference<Activity> activityWeakReference; MyHandler(Activity activity) { activityWeakReference = new WeakReference<>(activity); } @Override public void handleMessage(Message msg) { //在執行任務的時候,判斷弱引用所關聯的物件是否為空,能在物件已經被回收的情況下,不去執行不必要的任務 switch (msg.what) { case 1: if (activityWeakReference.get() != null) { //TODO } } } }工具的使用依然是profileApp,先用profile看出記憶體的變化情況。
問:如何判斷記憶體洩漏?答:記憶體洩漏是精細功夫,不能全盤觀察,只能憑藉profile的記憶體變化來推測。比如,開啟app之後記憶體一路飆升,直到超出app能夠使用的最大記憶體,app崩潰,,這是最明顯的。又比如,你反覆開啟關閉某一個介面,發現記憶體的穩定線(記憶體穩定之後,記憶體佔用值)隨著每一次的開啟關閉,都在提高,這說明,這一個介面上存在洩漏,有物件無法被回收。上一章節使用profile 最多是了解到 哪些物件的建立和回收引起了記憶體抖動,但是,涉及到洩漏,只通過profile尚且不能知道是哪個類持有了希望被回收的物件的強引用.
這裡就要藉助另外一款工具,他的名字叫做 Eclipse Mat (自行百度)
先回到剛才的profile,
點一下,然後再點一下,介面會自動跳轉:
然後:
但是這個檔案是無法直接在mat開啟的。
找到SDK目錄下的要hprof-conv.exe:
使用cmd命令,對檔案進行轉換,命令為:hprof-conv [原始檔名] [目標檔名]
如 hprof-conv 1.hprof 2.hprof 回車
將得到的2.hprof利用剛才下載的Mat工具開啟:
這個圖中會列出你dump的這一段記憶體中的所有物件,包括framework層的,也包括我們自己程式碼建立的物件。
案例模擬
我模擬了一個經典案例,也就是前面提到的Handler延時任務導致Activity不能被釋放,核心程式碼如下:
我就用一個非常普通的方式建立了一個handler物件,並且用它來執行一段延時任務,只不過,延時任務的延時時間是Integer的最大值,也就是說,任務要很久以後才會執行。之後,我反覆進出這一個Activity,然後按照上面的方式dump了一段 hprof,經過 hprof-conv 轉化,然後用Mat開啟:
結果如下:
我填寫過濾資訊:SecondActivity 回車:
在我們最終退出SecondActivity之後,記憶體中依然保留了18個無用的物件。
那麼是不是我們這18個都是洩漏的呢?
不一定。
前文講過,只有不合理的強引用,才會導致記憶體洩漏,所以我們要按照上面的方式排除軟弱虛引用。
之後我們能看到下面的介面,把能展開的資訊盡數展開:
了解Handler原始碼的同志們應該一眼就看明白了,handler引起了記憶體洩漏,是因為存在不合理地強引用鏈,
上圖中可以看出,最終是callback物件持有了 SecondActivity物件。
callback 任務的延時時間太長了,還沒有執行完,所以強引用不會給你釋放掉,而callback持有了Activity,導致Activity不能被釋放。
如何優化記憶體洩漏我們剛才已經看到了Handler的不合理使用導致了記憶體洩漏,那麼如果在onDestroy中移除所有的任務:
執行同樣的任務,dump下來的hprof 在mat:
觸發了GC之後,SecondActivity數量變為了0,記憶體洩漏解決。
當然還有另一種做法,靜態內部類+弱引用。
ps:靜態內部類是為了防止內部類持有外部類的引用,弱引用是為了在GC觸發之時,回收掉WeakRefrence中的物件。
但是排除之後,一個都沒有了。
小技巧上面的步驟雖然可行,但是如果有很多頁面都需要排查洩漏,那麼我們一個一個頁面去點開關閉,整個過程將會非常冗長難受。其實有辦法解決。
回到之前的直方圖:
使用方法為:如果你想進行一個操作,你操作前後各dump一個hprof,命名為 before和after, 然後用hprof-conv轉換一下,變為 before_ 和 after_ ,用eclipse mat同時開啟這兩個檔案,然後切換到after_.hprof ,點選上圖中的按鈕:
這種方式可以在處理洩漏之前,事先排查可能洩露的程式碼區域。簡化我們的優化工作。
結語記憶體抖動和洩漏優化涉及到Jvm很多知識點,除了我之前列出的幾點之外,還有很多細枝末節。要做好 記憶體優化,需要紮實的JVM知識基礎。
如果你的技術提升遇到瓶頸了,或者缺高階Android進階視訊學習提升自己,這有大量大廠面試題為你面試做準備!可以私聊我領取喔~
你的贊和關注是我繼續創作的動力~