眾所周知,java GC 是影響Android應用效能的主要因素之一。完全交給系統管理的GC往往不盡如人意,而開發者卻也毫無辦法,只能對著GC迎合啊迎合,想著辦法把GC哄開心了唄~
網上也不乏眾多的android 記憶體優化文章,成為開發者的編碼守則。但不管怎麼遵守,記憶體管理依然像一個黑盒子一樣,反正我是寫著不踏實。就比如下面這幾種情況:
- System.gc(),真的是隨叫隨到?
- 軟引用弱引用的錯誤使用
- 你覺得記憶體釋放了,它就真的釋放了麼?
幸得Android Monitor 提供了記憶體監視器,起碼開啟了一個視窗可以讓我們看看當前應用的記憶體到底是什麼樣的。 那麼現在我們就來通過一個小Demo,看看android 的GC到底是怎麼樣吧。
測試過程中,很悲痛的驗證了不同裝置不同系統的GC機制是不一樣的,我把它粗糙的區分為靈敏型和不靈敏性,所以,我們開發中還是小心小心再小心吧……例如:某些機型System.gc()會被立刻觸發,有些機型毫無響應。
概要
本次測試從以下幾個方面對Android GC 進行調研
- 主動呼叫System.gc(),不同狀態的GC時機
- 空白Activity所佔記憶體大小及GC時機
- 大記憶體量的Activity的GC時機
- 奔潰臨界值下,物件置NULL,是否還會引起記憶體溢位
- 記憶體抖動
- 軟引用、弱引用的使用
本次測試採用兩款手機:紅米3 .低端機型,手機記憶體值較低,測試環境更加嚴苛
主動呼叫GC下的GC時機
(紅米初始記憶體值)
測試1:建立臨時變數,通過主動呼叫System.gc()觀察
可以看到記憶體立刻增加了十幾MB,這裡我建立了一個Bitmap載入了一張比較大的圖片117.16KB,記憶體增加量是遠大於圖片大小的。
1 2 |
//並沒有進行顯示,僅是建立一個圖片 Bitmap bmp = BitmapFactory.decodeResource(getResources(), R.drawable.big_254); |
2分鐘後記憶體一直維持在這個水平,臨時變數的GC時機是完全不能保證的,我們可以理解為,GC執行緒還沒有轉到這個地方
之後我們多次呼叫System.gc(),記憶體監視視窗是沒有任何反應的。
測試2:初始化類成員變數,置NULL後,主動呼叫System.gc()觀察
1 2 3 4 5 6 7 |
this.bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.big_254); //置NULL this.bitmap = null; //GC System.gc(); |
GC依然沒有被觸發
發現更有意思的問題,每點選一次按鈕,記憶體消耗增加0.02MB,世界上果然沒有免費的午餐,點選事件又有新的物件產生了
測試3:不停的初始化類成員變數
也就是說不停的呼叫下面程式碼
1 |
this.bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.big_254); |
可以看到一串有趣的現象,似乎只有在記憶體值在41.78以上時,GC才會被觸發。
記憶體佔用只有達到一定限度時,GC才會開始執行。
期間也建立過臨時變數,GC也多次呼叫後全無反應。
空Activity佔用的記憶體大小及銷燬時間
初始值
初次開啟
返回後再次開啟
返回後再次開啟
初始(9.00MB)~ 初次開啟(9.40MB)~ 返回後再次開啟(9.55MB)~ 返回後再次開啟(9.69MB)
一個空白Activity的記憶體佔用量是0.4MB,返回後再次進入每次增加0.15MB左右,返回鍵每次增加0~0.01MB左右
迷之增長……
非空Activity的銷燬
通過下面程式碼增加Activity記憶體佔用量
1 2 3 4 5 |
Resources res = getResources(); for (int i = 0; i < 10; i++) { Bitmap bmp = BitmapFactory.decodeResource(res, R.drawable.test_100); this.bitmapList.add(bmp); } |
之後執行
1 2 |
System.gc(); finish(); |
無任何反應。
無論Activity是否佔用大量記憶體,其銷燬的時間都是遲鈍的。
瀕臨臨界值情況下,物件=NULL,再次消耗記憶體是否會溢位
經測算,當前測試機在190MB+時,將會記憶體溢位崩潰。
物件置NULL
1 |
this.bitmapList = null; |
再次增加並顯示
1 |
this.img.setImageResource(R.drawable.big_254); |
欣喜的事情發生了,“類成員置NULL,對防止記憶體溢位崩潰是有必要的”
迴圈內建立(big or small)物件是否會引起記憶體抖動
1 2 3 |
for (int i = 0; i < 20; i++) { Bitmap bmp = BitmapFactory.decodeResource(getResources(), R.drawable.test_100); } |
建立“小(1MB左右)”物件
1 2 3 |
for (int i = 0; i < 20; i++) { Bitmap bmp = BitmapFactory.decodeResource(getResources(), R.drawable.big_254); } |
迴圈內建立“大(10MB左右)”物件會引起嚴重的記憶體抖動
無論物件大小,都應避免在迴圈內建立物件
軟引用弱引用的錯誤使用
這個錯誤似乎很少會有人犯,但感覺還是列出來比較好
1 2 3 4 5 6 7 |
//錯誤使用 private SoftReference<List<Bitmap>> softReference = new SoftReference<List<Bitmap>>(new ArrayList<Bitmap>()); private WeakReference<List<Bitmap>> weakReference = new WeakReference<List<Bitmap>>(new ArrayList<Bitmap>()); //正確使用 private List<SoftReference<Bitmap>> listRefrence = new ArrayList<>(); |
用上述錯誤使用方式程式碼的兩種情況
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
List<Bitmap> list = softReference.get(); if (list == null) { list = new ArrayList<>(); softReference = new SoftReference<>(list); } Resources res = getResources(); for (int i = 0; i < 10; i++) { Bitmap bmp = BitmapFactory.decodeResource(res, R.drawable.big_254); list.add(bmp); } List<Bitmap> list = null; Resources res = getResources(); for (int i = 0; i < 10; i++) { Bitmap bmp = BitmapFactory.decodeResource(res, R.drawable.big_254); list = weakReference.get(); if (list == null) { list = new ArrayList<>(); weakReference = new WeakReference<>(list); } list.add(bmp); } |
記憶體會一直暴漲到奔潰。軟引用弱引用並不會被收回
1 2 3 4 5 6 |
//正確使用 Resources res = getResources(); for (int i = 0; i < 10; i++) { Bitmap bmp = BitmapFactory.decodeResource(res, R.drawable.big_254); right.add(new SoftReference<>(bmp)); } |
SoftReference 可以看到明顯的記憶體抖動,但是記憶體不會暴漲。
WeakReference 和 SoftReference 不同的是,牙更深一點,銷燬平率更大,驗證了弱引用比軟引用更容易被銷燬~
結論
- 不同系統不同型號的手機的GC機制是不同的
- System.gc()呼叫結果不同(立即GC或無反應)
- 廢棄記憶體回收頻率不同
- 大規模GC臨界值不同
- 對於載入圖片來說,記憶體增加量是遠大於圖片大小的。
- 臨時變數的GC時機是完全不能保證的,我們可以理解為,GC執行緒還沒有轉到這個地方。
- System.gc()並不是立刻執行GC的。
- 每點選一次按鈕,記憶體消耗增加0.02MB,世界上沒有免費的午餐,點選事件內部是會有新的物件產生的
- 有時,記憶體佔用只有達到一定限度時,GC才會開始被觸發。
- 一個空白Activity的記憶體佔用量是0.4MB,返回後再次進入每次增加0.15MB左右,返回鍵每次增加0~0.01MB左右
- 無論Activity是否佔用大量記憶體,其銷燬的時間都是遲鈍的。
- 類成員置NULL,對防止記憶體溢位崩潰是有必要的
- 無論物件大小,都應避免在迴圈內建立物件
- 注意軟引用與弱引用的正確使用
最後告誡一點:儘量不要在應用中呼叫System.gc(); 如果呼叫了System.gc()可能會為系統效能帶來嚴重的波動,即便呼叫System.gc()系統也未必立即響應去執行垃圾回收。