2.記憶體優化(二)優化分析

jarry發表於2019-02-11

1.記憶體與垃圾回收器

1.1.記憶體管理

不是所有指令都執行得又快又好,下面介紹記憶體及它如何影響系統執行。普遍認為,多數程式語言接近硬體或高效能,如C、C++和Fortran,通常程式設計師會自己管理記憶體,高手工程師對記憶體的分配,會慎重處理,並在未來結束使用時再次分配,一旦確認何時及怎樣分配記憶體,記憶體管理的品質就依賴於工程師的技能跟效率。實際情況是工程師們,不都會去追蹤那零碎的記憶體碎片。程式開發是個混亂又瘋狂的過程,記憶體通常都沒辦法完全被釋放,這些被囚禁的記憶體叫記憶體洩露。

記憶體洩露
記憶體洩露佔用了大量資源,這些資源其實可以更好地使用,為減少洩露引起的混亂、負擔、甚至資金損失,便有了記憶體管理語言。

跟蹤記憶體分配
這些語言在執行時跟蹤記憶體分配,以便當程式不再需要時釋放系統記憶體,完全不用工程師親自操作,這些記憶體回收藝術或科學,在記憶體管理環節下叫垃圾清理。這個設計概念在1959年,當初為了解決lisp語言問題,由John McCarthy發明的。

約翰麥卡錫-人工智慧之父

1.2.垃圾清理

垃圾清理的基本概念有: 第一,找到未來無法存取的資料,例如所有不受指令操控的記憶體。 第二,回收被利用過的資源。 原理簡單,但是兩百萬行編碼,跟4gigs的分配,在實際操作時卻非常困難。如果在程式中有20000個物件分配,垃圾清理會讓人困惑,哪一個是沒用的?或者,何時啟動垃圾清理釋放記憶體?這些問題其實很複雜。好在50年來,我們找到了解決問題的方法,就是Android Runtime中的垃圾清理。比McCarthy最初的方法更高階,速度快且是非侵入性的。經由分配型別,及系統如何有效地組織分配以利GC的執行,並作為新的配置。所有影響android runtime的記憶體堆都被分割到空間中,根據這些特點,哪些資料適合放到什麼空間,取決於哪個Android版本。

根據不同型別進行執行時記憶體的分配

最重要的一點是,每個空間都有預設的大小,在分配目標時要跟蹤綜合大小,且空間不斷地擴大,系統需要執行垃圾清理,以確保記憶體分配的正常執行,值得一提的是使用不同的Android runtime,GC的執行方式就會不同。例如在Dalvik中很多GC是停止事件,意思是很多指令的執行直到操作完成才會停止。

記憶體不足時GC處理

當這些GCs所用時間超過一般值,或者一大堆一起執行會耗費龐大的幀象時間,這是很麻煩的事情。

繪圖過程中GC回收

GC回收時間過長導致卡頓

GC回收時間過長導致卡頓

1.3.GC回收時間過長導致卡頓

Android工程師花費大量時間降低干擾,確保這些程式以最快的速度執行,話雖如此,在指令中影響程式執行的問題仍然存在,首先程式在任意幀內執行GCs所用的時間越多,消除少於16毫秒的呈像障礙,所必需的時間就會變少,如果有許多GCs或一大串指令一個接一個地操作,幀象時間很可能會超過16毫秒的呈像障礙,這會導致隱形的碰撞或閃躲。其次,指令流程可能造成GCs強制執行的次數增多,或者,執行時間超過正常值。例如,在一個長期執行的迴圈最內側分配囤積物件,很多資料就會汙染記憶體堆,馬上就會有許多GCs啟動,由於這一額外的記憶體壓力,雖然記憶體環境管理良好,計算比其他語言複雜,記憶體洩露仍會產生,這些漏洞在GCs啟動時,通過無法被釋放的資料汙染記憶體堆,嚴重降低可用空間的總量,並以常規方式強制GC的執行。就是這樣,如果要減少任意幀內啟動GC的次數,需要著重優化程式的記憶體使用量,從指令的角度看,或許很難追蹤這些問題的起因,但是,多虧Android SDK擁有一組不錯的工具。

2.記憶體分析的工具

2.1.Memory Monitor工具

我們來介紹一個叫作Memory Monitor的工具,Memory Monitor用於測試程式在一段時間後佔用了多少記憶體,下面來操作一下。點選開啟,然後會在Android Studio右下邊的視窗裡,開啟一個製表鍵,一旦發現在執行的程式,就會馬上開始記錄記憶體使用量,正如這裡所示,在Memory Monitor視窗的左上端,可以切換當前連線的裝置,右邊這裡可以選擇要監測的程式。幾乎佔用全部視窗的疊層圖,表示還有多少記憶體可用。深藍色的區域,表示當前正在使用中的記憶體總量,淺藍色或者淺灰色區域,表示空閒記憶體或者叫作未分配記憶體。圖表會在記憶體使用量變化時不斷更新,隨著時間推移,它也會不斷顯示可用記憶體量。隨著時間推移,它也會不斷顯示可用記憶體量,總之,如果程式都沒有在執行,圖表就完全是平坦的。

2.記憶體優化(二)優化分析

2.記憶體優化(二)優化分析

大量的垃圾回收事件

光從效能角度看,這是相當理想的狀態,但隨著程式分配跟記憶體釋放,圖表的分配總量也在跟著變化。如果要裝的程式急需大量記憶體,記憶體分配也急劇增加,顯示在空格里,不然的話,裝置記憶體不足會導致當機。所以對於記憶體分配,不管什麼時候都要特別小心,當垃圾清理開啟時就要特別留意記憶體量,在這個範例中垃圾清理運作良好。另外,如圖所示這裡也可能有問題,這裡有個程式佔用了大量記憶體,然後又一下子釋放了剛被佔用的記憶體。生成這些又細又窄的鋒線,不斷重複,這就是程式在花大量時間執行垃圾清理,執行垃圾清理所用的時間越多,其他可用時間就越少,像播放和傳送錄音。我們來看下實際情況。 momory monitor已經在監測Sunshine情況了,點選一個日期,看下具體內容,點選返回鍵,重複這個動作,記憶體就會持續被佔用,如這裡所顯示的。如果想要新的資料,只要改變幾次座標就行了,看下所得的天氣預報,不錯,星期三天氣明朗。記憶體被慢慢的佔用,最終,記憶體會被全部佔用,這種情況如果持續下去,垃圾清理就會啟動,釋放大塊的記憶體,這裡可以看到變化。要記得,因為Android記憶體管理系統是固有的,所以垃圾清理不會釋放所有的記憶體。我們的利器,可以強制執行單項的垃圾清理,在Memory Monitor的左上方有個garbage truck工具,單擊一下,就會開啟單項的垃圾清理,注意圖表右邊的變化。現在可以多點選幾次,再繼續點選,所有可被釋放的記憶體都會被釋放,裝置會恢復到初始狀態。接下來我們將瞭解記憶體洩露和heap viewer工具。

2.2.記憶體洩露

Android的Java語言有個最大的優點,是託管記憶體環境,物件在建立或消除時不用特別小心。這點儘管不錯,但也有些潛在的問題不易被發現。劃分到Android執行時的記憶體堆,是根據宣告型別和利於垃圾清理操作的角度來分配的,每一區域都有其預設的記憶體空間。

2.記憶體優化(二)優化分析
當一個程式所需的總儲存空間接近上限,垃圾清理就會啟動,刪除掉沒用的資料,一般情況下不用特別注意垃圾清理的執行。

2.記憶體優化(二)優化分析
但是大量的清理動作不斷地重複,很快地消耗掉幀像週期,花費在垃圾清理上的時間越多,播放或傳送錄音等事情的時間就越少。
2.記憶體優化(二)優化分析
工程師們製造的記憶體洩露,是垃圾清理執行的常見因素,記憶體洩露是不能被繼續使用的空間,但是垃圾收集器卻無法辨別出來,結果他們就一直存在於堆中,佔用有效空間,永遠無法被刪除,隨著記憶體不斷洩露,堆中的可用空間就不斷變小,這意味著為了執行常用的程式,垃圾清理需要啟動的次數越來越多。

記憶體洩漏表示的是不再用到的物件因為被錯誤引用而無法進行回收

搜尋跟修復洩露是個很棘手的問題,有些洩露很容易就會產生,例如對沒有使用的物件的迴圈引用。不過有些也很複雜,例如,在類別載入器安裝未完成就強制執行,不管怎樣,一個程式想要執行得又快又好,就需留意可能存在的記憶體洩露。你的程式碼將允許在各種各樣的裝置上,又互相結合,不是所有的資料都佔用同樣的記憶體,不過,還在有一個簡單的工具,可以檢視Android SDK中潛在的漏洞。

2.記憶體優化(二)優化分析

2.3.Heap Viewer工具

Heap Viewer是個很簡單的工具,利用它可以檢視記憶體狀態,以及空間佔用率的情況。通過Heap Viewer可知程式在特定時間內的記憶體使用量,跟原來一樣,先在裝置上開啟Android Studio裡的sunshine,在執行start Heap Viewer前,先開啟Android Device Monitor。 我們看到,每次垃圾清理後,Heap都會更新,點選Cause GC,發現所有的資料都更新了,更新後的表格顯示,在Heap上哪些資料是可用的,選中其中任一行資料,就可以看到詳細資料,點選class object,螢幕上馬上出現大量更新的資料,矩形圖列出這一資料記憶體分配的數量,跟確切的容量。我們這裡討論的是class object,heap viewer可以有效地分析程式在堆中所分配的資料型別,以及數量和大小。這裡列出在堆中各別型別程式的總容量,例如,這兩個在堆裡超過1400的資料組,用掉約1200個千位元組,而這個只有27的資料組,卻佔用了約2個兆位元組。heap viewer能夠準確地,辨別出程式分配的型別和數量,以及各自在堆中的容量。比方說,這個27的資料組佔用了近2兆的位元組,可這4個2000的資料組,目前佔用了228個千位元組。在搜尋記憶體漏洞時,這是個相當不錯的工具。

Heap Viewer可以看到當前程式中的Heap Size的情況,分別有哪些型別的資料、佔比

2.4.使用Memory Monitor觀察記憶體洩露

討論下記憶體洩露的問題,記憶體洩露的行蹤,常常神出鬼沒,常慢慢不動聲色的出現,有時要幾天或幾個星期後,才會被發現。實際上,可能到程式莫名其妙地操作緩慢時,才會發現記憶體不足的問題。只要用對工具,耐心分析,解決記憶體洩露不是難事。首先用Memory Monitor,觀察漏洞是怎樣生成的,在下一個影片中,再利用Heap Viewer做初步確認。舉例說明漏洞的生成,以及SDK工具,如何偵測這樣微小的漏洞,先把手機旋轉幾下,然後開啟Memory Monitor,這樣做的目的是要說明,一個簡單的動作就會產生漏洞。像這樣不斷改變手機方向,就會有漏洞產生,聽起來很奇怪,但是藉由這一動作,可知漏洞是怎麼緩慢且隱祕地產生的。首先,漏洞慢慢吞噬程式內的可用記憶體,直到GC的啟動,再來,值得注意的是由於程式上有漏洞,導致GC無法回收全部垃圾。結果大約30秒後,就會啟動第二次GC,當漏洞吞噬所有的可用記憶體時,Android調整並分配給程式更高的記憶體上限。這樣做的同時,如果漏洞沒有修復,記憶體會不斷地被吞噬,結果導致系統無法再配置,手機也就沒辦法再用了,最後當機。稍等下,第三次的GC就會啟動,第四次跟前兩次類似,現在這組指令在持續執行,系統分配更多的記憶體量,可以用同樣的方法操作Heap Viewer。

不斷旋轉螢幕導致記憶體慢慢被吞噬

2.5.使用Heap Viewer觀察記憶體洩露

通過Heap Viewer,可知第一次GC僅釋放了1.39兆記憶體,這種結果顯示,因為漏洞的存在,垃圾清理無法回收全部垃圾。Heap viewer顯示第二次GC後,系統必須經由配置更多的記憶體,來調整記憶體量。堆從第一次GC的20兆,增加到32兆,此次Java堆釋放了12.9兆,這是,系統不斷地為程式配置更多的記憶體。以上動作如果一再重複,系統終會無法配置記憶體,程式也就掛了。切記,記憶體漏洞非常緩慢又不易被發現,需要時間,跟適當的環境來確認,有時,這樣的資料,也表示記憶體的正當存取。比如,處理圖片跟照片的程式,表面看似記憶體在洩露,實際上它針對核心功能的儲存器,不停地進行資料評估。因此,要明白記憶體洩露如何顯示在SD上,也要清楚,記憶體洩露如何顯示在擁有SDK的工具上,如Memory Monitor和Heap Viewer。但是,各位可能不知道他們源於何地,以下這些方法可以防止漏洞的出現。利用編碼檢視程式的壽命,清理不用的檔案,接下來,辨別漏洞產生的原因。

每旋轉一次,Allocated值不斷上升,堆記憶體值(Heap Size)不斷上升

2.6.追蹤記憶體洩露的程式碼

檢視自定義控制元件init方法中如下程式碼:

private void init() {
	ListenerCollector collector = new ListenerCollector();
	collector.setListener(this, mListener);
}
複製程式碼

儲存一個Activity中所有檢視監聽器,這個想法看似無害,但如果你忘了清理它們,你可能會不經意地造成一個緩慢的洩漏。相關程式碼:

collector.setListener(this, mListener);
複製程式碼

當Activity被銷燬和建立時,這一問題被複雜化。在示例中,由於裝置的方向變化使一個新的Activity建立,相關聯的監聽被建立,但是當Activity被銷燬時,該監聽永遠不會被釋放。這意味著,監聽無法被GC回收,這裡導致了記憶體洩露。當裝置旋轉並呼叫當前Activity的onStop方法時,一定要清理所有檢視的監聽。

2.7.使用Allocation Tracker觀察記憶體洩露

另外,分配追蹤器,可以辨別額外的記憶體膨脹,這是由於記憶體的歷史瀏覽記錄不斷擴充產生的。選擇一組仍在堆中的資料或者程式,這組資料堆中,在這個操作裡,堆中資料叫作onCreate。這樣一來,手機每旋轉一次就有新的動作,類似的資料組,基本上就會在堆中膨脹。所以,如果在漏洞存在時旋轉手機,垃圾清理無法清除這些資料,就會在堆中產生大量的垃圾。藉由分配追蹤器,可以弄清這一問題。

3.記憶體抖動

3.1.什麼是記憶體抖動?

我們解決了哪些討厭的洩露,現在遇到了更大的問題,記憶體抖動。要知道,堆記憶體都有一定的大小,能容納的資料是有限制的,當Java堆的大小太大時,垃圾收集會啟動停止堆中不再應用的物件,來釋放記憶體。現在,記憶體抖動這個術語可用於描述在極短時間內分配給物件的過程。例如,當你在迴圈語句中配置一系列臨時物件,或者在繪圖功能中配置大量物件時,這相當於內迴圈,當螢幕需要重新繪製或出現動畫時,你需要一幀幀使用這些功能,不過它會迅速增加你的堆的壓力。這兩種情況下,我們都制定瞭解決方案,可在短時間內創造大量的物件。根據創造的物件的量,或者每個物件的大小,你可能很快就消耗掉所有剩餘記憶體,導致垃圾收集強行開啟。隨著它們的開啟執行,會消耗更多寶貴的幀時間,所以,高效能的應用很有必要,你需要鑑別並從內迴圈裡,取消會被重複執行的程式碼配置。為了更好的尋找到這些程式碼配置,Android Studio為此特別打造了一個方便的工具。

記憶體抖動

3.2.使用Allocation Tracker

現在看一下你的應用記憶體分配圖,這能有效的獲悉大部分資料到底用在哪裡,以及正在分配哪種型別的資料,這能幫你找到現有的不必要分配的資料。可惜Heap Viewer不能顯示你的資料具體分配在程式碼的何處,為此,我們需要一個叫做分配追蹤器的工具。和以前一樣,我們開啟Android Studio Device Monitor,在前臺載入Sunshine,開啟DDMS檢視點選start allocation tracking按鈕,然後使用應用,隔一段時間在點選stop allocation tracking按鈕。停止之後在DDMS出現了一個列表,這個列表顯示了你在使用應用期間,所有的分配情況,這裡的每一行都代表不同的分配,allocation order這一欄會提示你,分配進行的具體時間,分配類別這一欄顯示了分配資料的型別,以及大小,還有其他資訊來告訴你哪個執行緒具體決定了這一分配。最後,分配站這一欄告訴你程式碼的哪一個功能實際分配了記憶體。比如,我們選擇整型,測試的值決定了這個整型的分配,如果你點選一個分配,你可以看見完整的呼叫堆疊。這個表格包含大量資訊!

2.記憶體優化(二)優化分析

3.3.通過Trace View找出記憶體抖動

本次練習,我們來執行記憶體抖動活動。下面點這個按鈕,對陣列來點有意思的事情,你會發現跳著舞的海盜會暫停,但最後都會接著跳舞。這就是討厭的卡頓,讓我們解決它吧。通過跟蹤顯示來剖析這個活動,開啟trace view的皮膚,注意短時間內發生的頻繁的垃圾收集活動,可能會傷害到應用的效能。記住,我們還可以採集這個記憶體監控器影象,這個截圖展示了記憶體抖動是怎樣通過Memory Monitor清晰顯示的。

2.記憶體優化(二)優化分析

3.4.什麼導致了記憶體抖動

我們已經使用SDK工具採集足夠多的資料,能知道記憶體抖動情況出現的時間,現在來揪出導致這種情況的程式碼吧。Trace View給我們提供了一個方法,讓我們仔細看一下在主執行緒裡,選擇方式時的資料配置檔案,當你選擇主執行緒方式時,你會發現反覆出現的Java字串賦值操作,比如這個。再看呼叫堆疊,我們會更加確定資料佇列副本被運用於擴大字串緩衝。來看MemoryChurnActivity的原始碼,正如OnClickListener所顯示,我們稱此功能為imPrettySureSortingIsFree,讓我們來看這個程式碼。此處的方法叫作imPrettySureSortingIsFree,這個程式碼產生了新的字串,通過字串連線每次都有一個單元值,看一下我說的這個程式碼的指導提示,但是,出現連線的地方比較特別。這個初看起來似乎沒什麼問題,為什麼這個程式碼會導致記憶體抖動? 頻繁使用垃圾清理會造成兩種後果,一是,每個單元值的連結都會生成新的字元陣列,這是因為,在迴圈之內驟然接到重複指令組合而成,二是,通過定位追蹤器,確認字元陣列的膨脹,更新一下資料,在下一節中,向大家介紹所得的結果。

3.5.修改程式碼減少記憶體抖動

我們可以在我們的程式碼進行小的調整,以防止記憶體抖動。讓我們來看看對比圖,而不是在一個時間串聯一個單元格值打造每一行,讓我們使用一個StringBuilder例項,並用一個字串構造每一行,需要注意的是StringBuilder中的例項化的迴圈外。因此它的記憶體分配一次,然後,我們只是作為一個緩衝,在每次迴圈我們先清除它,然後我們追加,整數的一個字串來表示對於迴圈迭代的行。更多細節見導師的筆記到這個程式碼段,執行memory_churn_optimized,確認我們減少的GC在短期時間窗中發生的量,您也可以使用allocation tracker驗證。現在對於我們來說,即使修改了程式碼,海盜動畫仍然會出現卡頓的現象,這意味著該處理放到後臺處理可能更加合適。

2.記憶體優化(二)優化分析

時間變短,且有時間間隔

4.工具的特色

1)Memory Monitor獲得記憶體的動態檢視 2)Heap Viewer顯示堆記憶體中儲存了什麼 3)Allocation Tracker 具體是哪些程式碼使用了記憶體

相關文章