什么是垃圾回收
- 對比C/C++這種需要自己管理內存的語言
- java可以實現自動內存管理和回收
- 垃圾回收器負責回收程序中已經不使用,但是仍然被各種對象占用的內存。
優點:將程序員從繁重、危險的內存管理工作中解放出來
缺點:可能會占用大量資源
垃圾回收機制
Android系統里面有一個Generational Heap Memory的模型,系統會根據內存中不同的內存數據類型分別執行不同的GC操作。
執行GC操作的時候,任何線程的任何操作都會需要暫停,等待GC操作完成之后,其他操作才能夠繼續運行。
通常來說,單個的GC并不會占用太多時間,但是大量不停的GC操作則會顯著占用幀間隔時間(16ms)。如果在幀間隔時間里面做了過多的GC操作,那么自然其他類似計算,渲染等操作的可用時間就變得少了。
年輕代(young generation)
年輕代是所有新對象產生的地方。當年輕代內存空間被用完時,就會觸發垃圾回收。這個垃圾回收叫做Minor GC。年輕代被分為3個部分——Enden區和兩個Survivor區。
- 大多數新建的對象都位于Eden區。
- 當Eden區被對象填滿時,就會執行Minor GC。并把所有存活下來的對象轉移到其中一個survivor區。
- Minor GC同樣會檢查存活下來的對象,并把它們轉移到另一個survivor區。這樣在一段時間內,總會有一個空的survivor區。
- 經過多次GC周期后,仍然存活下來的對象會被轉移到年老代內存空間。通常這是在年輕代有資格提升到年老代前通過設定年齡閾值來完成的。
老年代(Old Generation)
年老代內存里包含了長期存活的對象和經過多次Minor GC后依然存活下來的對象。通常會在老年代內存被占滿時進行垃圾回收。老年代的垃圾收集叫做Major GC。Major GC會花費更多的時間。
永久代(Permanent Generation)
存放方法區,方法區中有要加載的類信息、靜態變量、final類型的常量、屬性和方法信息。
導致GC頻繁執行有兩個原因:
1.內存抖動
2.瞬間產生大量的對象會嚴重占用Young Generation的內存區域,當達到閥值,剩余空間不夠的時候,也會觸發GC。即使每次分配的對象占用了很少的內存,但是他們疊加在一起會增加Heap的壓力,從而觸發更多其他類型的GC。這個操作有可能會影響到幀率,并使得用戶感知到性能問題。
Android內存檢測工具
1. Memory Monitor
android monitor能做什么:
a. 實時查看應用的內存分配情況
b. 判斷應用是否由于GC操作造成卡頓
c. 判斷應用崩潰是否是因為超出了內存
如何使用 Memory Monitor
進入項目后,可以看到Android Studio的主面板左下角有一個Android Monitor標簽:
點擊Android Monitor標簽,然后點擊Monitor標簽,當項目運行的時候即可查看內存實時數據
在Android Monitor中我們可以手動觸發GC,下圖中的小車子就是觸發GC的按鈕,一旦按下就會回收那些沒有被引用的對象
利用 Memory Monitor可以發現的問題
1.發現Memory Churn內存抖動,內存抖動是因為大量的對象被創建又在短時間內馬上被釋放。
2.發現大內存對象分配的場景
3.發現內存不斷增長的場景
4.確定卡頓問題是否因為執行了GC操作
2. Allocation Tracker
Allocation Tracker是android studio的一個內存分配跟蹤器,使用之后能夠了解在一定時間內的內存分配情況。
如何使用Allocation Tracker
點擊下圖中畫紅圈的按鈕可以啟動標記
再次點擊停止追蹤
隨后自動生成一個alloc結尾的文件,這個文件就記錄了這次追蹤到的所有數據,然后會打開一個數據面板,面板左上角是所有歷史數據文件列表,后面是詳細信息
查看方式
- Group by Method:用方法來分類我們的內存分配
- Group by Allocator:用內存分配器來分類我們的內存分配
默認會以Group by Method來組織。首先以線程對象分類,默認以分配順序來排序。Count表示分配了多少次內存,size表示內存大小
以Group by Allocator來查看內存分配的情況如下圖。這種方式顯示的好處,是我們很好的定位我們自己的代碼的分析信息
Jump To Source按鈕
如果我們想看內存分配的實際在源碼中發生的地方,可以選擇需要跳轉的對象,點擊該下圖中紅圈的按鈕就能發現我們的源碼
統計圖標按鈕
Jump To Source按鈕右邊的按鈕為統計按鈕,點擊該按鈕,會彈出一個新窗口,里面是一個酷炫的統計圖標,有柱狀圖和輪胎圖兩種圖形可供選擇,默認是輪胎圖,其中分配比例可以選擇分配次數和占用內存大小,默認是大小Size
圓心是我們的起點處,如果把鼠標放到我圖中標注的區域,會在右邊顯示當前指示的是什么線程以及具體信息。
默認打開的是全局信息,我們如果想看其中某個線程,詳細信息,可以順著某個扇面向外圍滑動,當然如果你覺得不還是不清晰,可以雙擊該扇面全面展現該扇面的信息。如果想回到默認顯示的圓,雙擊圓心空白處就可以。
3.Heap Viewer
Heap Viewer能實時查看App分配的內存大小和空閑內存大小。還可以用于發現發現Memory Leaks。但這個功能只限于Android5.0以上。
如何使用Heap Viewer
在Android studio工具欄中直接點擊小機器人:
選中要檢測的app,然后點擊cause GC:
總覽
列名 | 含義 |
---|---|
Heap Size | 堆棧分配給App的內存大小 |
Allocated | 已分配使用的內存大小 |
Free | 空閑的內存大小 |
%Used | Allocated/Heap Size,使用率 |
Objects | 對象數量 |
詳情
類型 | 意義 |
---|---|
free | 空閑的對象 |
data object | 數據對象,類類型對象,最主要的觀察對象 |
class object | 類類型的引用對象 |
1-byte array(byte[],boolean[]) | 一個字節的數組對象 |
2-byte array(short[],char[]) | 兩個字節的數組對象 |
4-byte array(long[],double[]) | 4個字節的數組對象 |
non-Java object | 非Java對象 |
當我們點擊某一行時,可以看到如下的柱狀圖:
橫坐標是對象的內存大小,這些值隨著不同對象是不同的,縱坐標是在某個內存大小上的對象的數量
那么如何用heap viewer來檢測內存泄露呢?在需要檢測內存泄漏的用例執行過后,手動GC下,然后觀察總覽中的allocted(也可以觀察Allocated/Heap Size內存的情況),看看內存是不是會回到一個穩定值,多次操作后,只要內存是穩定在某個值,那么說明沒有內存溢出的,如果發現內存在每次GC后,都在增長,不管是慢增長還是快速增長,都說明有內存泄漏的可能性。
4. LeakCanary
這是一個是一個開源的庫。github地址為LeakCanary。使用之后能夠直接在手機或者虛擬機發生明顯的內存泄漏之后彈出通知欄,告知在哪個類中出現問題。
下圖的例子是因為一個static靜態的TextView在所在的Activity被銷毀時沒有被回收而引起的,只要把TextView前的static去掉就可以了。
避免內存泄露的方法
- 盡量不要讓靜態變量引用activity
- 使用WeakReference
- 使用靜態內部類來代替內部類
- 靜態內部類使用弱引用引用外部類
- 在聲明周期結束的時候釋放資源
減少內存使用的方法
- 使用更輕量的數據結構(比如SpareArray代替HashMap)
- 避免在onDraw方法中創建對象
- 對象池(Message.obtain())
- LRUCache
- Bitmap內存復用,壓縮(inSampleSize, inBitmap)
- StringBuilder
實例分析(MemoryBugs)
首先打開應用,點擊startActivityB按鈕,等待一段時間后彈出通知,點擊通知顯示如下圖
從圖中可以看出內存泄露是因為當MainActivity關閉之后,sTextView仍然持有MainActivity引用,導致無法回收MainActivity的引用。
我們再點擊dump java heap
彈出heap viewer顯示如下,從圖中可以看到MainActivity仍然存在于內存中。具體的解決辦法就是取消將sTextView聲明為static。
將sTextView聲明為static后,將代碼中的延遲從5000改為20000
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
System.out.println("post delayed may leak");
}
}, 20000);
發現當點擊跳轉到ActivityB之后,通知提示如下圖
這是因為Handler仍然持有MainActivity的引用
于是習慣性的將Handler聲明為靜態內部類
public static class MyHandler extends Handler {
private WeakReference<MainActivity> mWeakReference;
public MyHandler(MainActivity activity) {
mWeakReference = new WeakReference<MainActivity>(activity);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
}
private MyHandler mHandler;
protected void onCreate(Bundle savedInstanceState) {
mHandler = new MyHandler(this);
}
但是仍然出現了內存泄露,于是看了下內存泄露的具體原因,似乎是匿名接口Runnable持有了對MainActivity的引用,于是創建一個Runnable靜態內部類
public static class MyRunnable implements Runnable{
public final WeakReference<MainActivity> mMainActivityWeakReference;
public MyRunnable(MainActivity activity) {
mMainActivityWeakReference = new WeakReference<>(activity);
}
@Override
public void run() {
System.out.println("post delayed may leak");
}
}
private Runnable mRunnable;
protected void onCreate(Bundle savedInstanceState) {
mRunnable = new MyRunnable(this);
}
mHandler.postDelayed(mRunnable, 20000);
這是后再利用Heap Viewer驗證,發現MainActivity的引用已經不存在了
接下來點擊start allocation按鈕,為了方便查看循環是否完成,在startAllocationLargeNumbersOfObjects方法最后添加
Toast.makeText(this, "完成循環", Toast.LENGTH_SHORT).show();
點擊后,內存圖如下所示,
如果連續點擊,則會出現內存抖動
點擊前start allocation tracking,當出現提示完成循環后結束tracking,結果如下圖,發現有10000個Rect對象和10000個StringBuilder對象
具體優化方法是,只在onCreate()方法中新建一個Rect對象,并且將要輸出的String事先定義
private Rect mRect;
private String printString;
protected void onCreate(Bundle savedInstanceState) {
mRect=new Rect(0, 0, 100, 100);
printString= "-------: " + mRect.width();
}
private void startAllocationLargeNumbersOfObjects() {
Toast.makeText(this, R.string.memorymonitor, Toast.LENGTH_SHORT).show();
for (int i = 0; i < 10000; i++) {
System.out.println(printString);
}
}
最后將MyView類中的onDraw()方法中新建對象的操作放到聲明變量的時候進行
private RectF rect = new RectF(0, 0, 100, 100);
private Paint paint = new Paint();