1.記憶體優化(一)記憶體洩漏

jarry發表於2019-02-11

目錄

  1. 記憶體洩漏
  1. 如何找到專案中存在的記憶體洩露
  1. 如何在應用裡面避免記憶體洩露

  2. 記憶體洩漏經常出現的例子

1.記憶體洩漏

C/C++ 自己去分配記憶體和釋放記憶體——手動管理malloc和free

1.1.什麼是記憶體洩露

記憶體不在GC掌控之內了。

當一個物件已經不需要再使用了,本該被回收時,而有另外一個正在使用的物件持有它的引用從而就導致物件不能被回收。這種導致了本該被回收的物件不能被回收而停留在堆記憶體中,就產生了記憶體洩漏

瞭解java的GC記憶體回收機制:某物件不再有任何的引用的時候才會進行回收。

ArrayList<String> list = new Arraylist<String>();
複製程式碼

(回到頂部)

1.2.瞭解記憶體分配的幾種策略

1.靜態的

靜態的儲存區:記憶體在程式編譯的時候就已經分配好,這塊的記憶體在程式整個執行期間都一直存在。 它主要存放靜態資料、全域性的static資料和一些常量。

2.棧式的

在執行函式(方法)時,函式一些內部變數的儲存都可以放在棧上面建立,函式執行結束的時候這些儲存單元就會自動被釋放掉。 棧記憶體包括分配的運算速度很快,因為內建在處理器的裡面的。當然容量有限。

3.堆式的

也叫做動態記憶體分配。有時候可以用malloc或者new來申請分配一個記憶體。在C/C++可能需要自己負責釋放(java裡面直接依賴GC機制)。

在C/C++這裡是可以自己掌控記憶體的,需要有很高的素養來解決記憶體的問題。java在這一塊貌似程式設計師沒有很好的方法自己去解決垃圾記憶體,需要的是程式設計的時候就要注意自己良好的程式設計習慣。

區別:

1.空間和大小: 堆是不連續的記憶體區域,堆空間比較靈活也特別大。 棧式一塊連續的記憶體區域,大小是有作業系統覺決定的。

2.效率: 堆管理很麻煩,頻繁地new/remove會造成大量的記憶體碎片,這樣就會慢慢導致效率低下。 對於棧的話,他先進後出,進出完全不會產生碎片,執行效率高且穩定。

public class Main{
	int a = 1;
	Student s = new Student();
	public void XXX(){
		int b = 1;//棧裡面
		Student s2 = new Student();
	}
}
複製程式碼

1.成員變數全部儲存在堆中(包括基本資料型別,引用及引用的物件實體)---因為他們屬於類,類物件最終還是要被new出來的。

2.區域性變數的基本資料型別和引用儲存於棧當中,引用的物件實體儲存在堆中。-----因為他們屬於方法當中的變數,生命週期會隨著方法一起結束。

我們所討論記憶體洩露,主要討論堆記憶體,他存放的就是引用指向的物件實體。

(回到頂部)

1.3.防止記憶體溢位

有時候確實會有一種情況:當需要的時候可以訪問,當不需要的時候可以被回收也可以被暫時儲存以備重複使用。比如:ListView或者GridView、RecyclerView.

載入大量資料或者圖片的時候,圖片非常佔用記憶體,一定要管理好記憶體,不然很容易記憶體溢位。滑出去的圖片就回收,節省記憶體。看ListView的原始碼——回收物件,還會重用ConvertView。如果使用者反覆滑動或者下面還有同樣的圖片,就會造成多次重複IO(很耗時),那麼需要快取---平衡好記憶體大小和IO,演算法和一些特殊的java類。

演算法:lrucache(最近最少使用先回收)

特殊的java類:利於回收,StrongReference,SoftReference,WeakReference,PhatomReference

型別 回收時機 使用 生命週期
StrongReference 強引用 從不回收 物件的一般儲存 JVM停止的時候才會終止
SoftReference 軟引用 當記憶體不足的時候 SoftReference結合
ReferenceQueue構造有效期短
記憶體不足時終止
WeakReference 弱引用 在垃圾回收的時候 同軟引用 GC後終止
PhatomReference 虛引用 在垃圾回收的時候 合ReferenceQueue來跟蹤物件被
垃圾回收期回收的活動
GC後終止

開發時,為了防止記憶體溢位,處理一些比較佔用記憶體大並且生命週期長的物件的時候,可以儘量使用軟引用和弱引用。 軟引用比LRU演算法更加任性,回收量是比較大的,你無法控制回收哪些物件。

比如使用場景:預設頭像、預設圖示。 ListView或者GridView、RecyclerView要使用記憶體快取+外部快取(SD卡)

(回到頂部)

1.4.記憶體洩露例子

單例模式導致記憶體物件無法釋放而導致記憶體洩露

MainActivity在記憶體當中洩露了。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        CommUtil commUtil = CommUtil.getInstance(this);

    }
}


public class CommUtil {
    private static CommUtil instance;
    private Context context;
    private CommUtil(Context context){
        this.context = context;
    }

    public static CommUtil getInstance(Context context){
        if(instance == null){
            instance = new CommUtil(context);
        }
        return instance;
    }

}
複製程式碼

這個故事告訴我們能用Application的context就用Application的CommonUtil生命週期跟MainActivity不一致,而是跟Application程式同生同死。

旋轉3次:會在記憶體裡面開闢三個MainActivity 實際上3次以上都只會有2個MainActivity。當GC回收的時候會將除了第0個和最後這一個留著其他的都會被回收。

優化兩個情況: 1.主動;平時 2.被動,很卡的時候 出現問題的時候。

如果我們不知道程式碼記憶體洩露的情況,如何判斷我們的專案裡面有哪些是有記憶體洩露情況的?

1.憑藉工具結合自己的經驗來判斷。 往往我們的app在某個時候或者某個操作以後會出現很卡的現象。

1)判斷就是檢視記憶體抖動情況

Android Monitor MAT (對Eclipse外掛使用的,也有獨立分析工具)

查詢引用了該物件的外部物件有哪些, 然後一個一個去猜,查詢可能記憶體洩露的嫌疑犯,依據:看(讀程式碼和猜)他們的生命週期是否一致(可以通過快照對比),如果生命週期一致了肯定不是元凶。

排除一些容易被回收的(軟引用、虛引用、弱引用)

設定監聽很容易出現記憶體洩露

handler.post(callback)
onDestroy(){
handler.removeCallback();
}
複製程式碼

(回到頂部)

2.如何找到專案中存在的記憶體洩露

往往做專案的時候情況非常複雜,或者專案做得差不多了想起來要效能優化檢查下記憶體洩露。

如何找到專案中存在的記憶體洩露的這些地方呢?

2.1.確定是否存在記憶體洩露

1.Android Monitors的記憶體分析

最直觀的看記憶體增長情況,知道該動作是否發生記憶體洩露。

動作發生之前:GC完後記憶體1.4M; 動作發生之後:GC完後記憶體1.6M

2.使用MAT記憶體分析工具

MAT分析heap的總記憶體佔用大小來初步判斷是否存在洩露。 Heap檢視中有一個Type叫做data object,即資料物件,也就是我們的程式中大量存在的類型別的物件。在data object一行中有一列是“Total Size”,其值就是當前程式中所有Java資料物件的記憶體總量,一般情況下,這個值的大小決定了是否會有記憶體洩漏。

我們反覆執行某一個操作並同時執行GC排除可以回收掉的記憶體,注意觀察data object的Total Size值,正常情況下Total Size值都會穩定在一個有限的範圍內,也就是說由於程式中的的程式碼良好,沒有造成物件不被垃圾回收的情況。

反之如果程式碼中存在沒有釋放物件引用的情況,隨著操作次數的增多Total Size的值會越來越大。 那麼這裡就已經初步判斷這個操作導致了記憶體洩露的情況。

(回到頂部)

2.2.先找懷疑物件(哪些物件屬於洩露的)

MAT對比操作前後的hprof來定位記憶體洩露是洩露了什麼資料物件。(這樣做可以排除一些物件,不用後面去檢視所有被引用的物件是否是嫌疑)

快速定位到操作前後所持有的物件哪些是增加了(GC後還是比之前多出來的物件就可能是洩露物件嫌疑犯)

技巧:Histogram中還可以對物件進行Group,比如選擇Group By Package更方便檢視自己Package中的物件資訊。

(回到頂部)

2.3. MAT分析hprof來定位記憶體洩露的原因所在。(哪個物件持有了上面懷疑出來的發生洩露的物件)

  • 1.Dump出記憶體洩露“當時”的記憶體映象hprof,分析懷疑洩露的類;
  • 2.把上面得出的這些嫌疑犯一個一個排查個遍。步驟:

(1)進入Histogram,過濾出某一個嫌疑物件類

(2)然後分析持有此類物件引用的外部物件(在該類上面點選右鍵List Objects--->with incoming references)

(3)再過濾掉一些弱引用、軟引用、虛引用,因為它們遲早可以被GC幹掉不屬於記憶體洩露(在類上面點選右鍵Merge Shortest Paths to GC Roots--->exclude all phantom/weak/soft etc.references)

(4)逐個分析每個物件的GC路徑是否正常,此時就要進入程式碼分析此時這個物件的引用持有是否合理,這就要考經驗和體力了!

(比如上面的例子中:旋轉螢幕後MainActivity有兩個,肯定MainActivity發生洩露了,那誰導致他洩露的呢?原來是我們的CommonUtils類持有了旋轉之前的那個MainActivity,那是否合理?結合邏輯判斷當然不合理,由此找到記憶體洩露根源是CommonUtils類持有了該MainActivity例項造成的。怎麼解決?罪魁禍首找到了,怎麼解決應該不難了,不同情況解決辦法不一樣,要靠你的智慧了。)

context.getapplictioncontext()可以嗎? 可以!!只要讓CommonUtils類不直接只有MainActivity的例項就可以了。

一般我是最笨的方法解決
new出來物件,用完後把它 = null;這樣算不算優化
假如:
    方法裡面定義的物件,要去管嗎?一般不需要管。
    自己=null,要自己去控制所有物件的生命週期 判斷各種空指標,有點麻煩。
    但是在很多時候去想到主動將物件置為null是很好的習慣。
複製程式碼

(回到頂部)

3.如何在應用裡面避免記憶體洩露

判斷一個應用裡面記憶體洩露避免得很好,怎麼看?
當app退出的時候,這個程式裡面所有的物件應該就都被回收了,尤其是很容易被洩露的(View,Activity)是否還記憶體當中。
可以讓app退出以後,檢視系統該程式裡面的所有的View、Activity物件是否為0.

工具:

使用AndroidStudio--AndroidMonitor--System Information--Memory Usage檢視Objects裡面的views和Activity的數量是否為0.
命令列模式:

(回到頂部)

4.記憶體洩露經常出現的例子

記憶體洩露(Memory Leak):

程式中某些物件已經沒有使用價值了,但是他們卻還可以直接或者間接地被引用到GC Root導致無法回收。
當記憶體洩露過多的時候,再加上應用本身佔用的記憶體,日積月累最終就會導致記憶體溢位OOM.

記憶體溢位(OOM):

當應用佔用的heap資源超過了Dalvik虛擬機器分配的記憶體就會記憶體溢位。比如:載入大圖片。

4.1.靜態變數引起的記憶體洩露

當呼叫getInstance時,如果傳入的context是Activity的context。只要這個單利沒有被釋放,那麼這個 Activity也不會被釋放一直到程式退出才會釋放。

public class CommUtil {
	    private static CommUtil instance;
	    private Context context;
	    private CommUtil(Context context){
		this.context = context;
	    }

	    public static CommUtil getInstance(Context mcontext){
		if(instance == null){
		    instance = new CommUtil(mcontext);
		}
	//        else{
	//            instance.setContext(mcontext);
	//        }
		return instance;
	    }
複製程式碼

(回到頂部)

4.2.非靜態內部類引起記憶體洩露(包括匿名內部類)

錯誤的示範:

public void loadData(){//隱式持有MainActivity例項。MainActivity.this.a
	new Thread(new Runnable() {
	    @Override
	    public void run() {
		while(true){
		    try {
			//int b=a;
			Thread.sleep(1000);
		    } catch (InterruptedException e) {
			e.printStackTrace();
		    }
		}
	    }
	}).start();
}
複製程式碼

解決方案: 將非靜態內部類修改為靜態內部類。(靜態內部類不會隱式持有外部類)

當使用軟引用或者弱引用的時候,MainActivity難道很容易或者可以被GC回收嗎?
GC回收的機制是什麼?
當MainActivity不被任何的物件引用。 雖然Handler裡面用的是軟引用/弱引用,但是並不意味著不存在其他的物件引用該MainActivity。
我連MainActivity都被回收了,那他裡面的Handler還玩個屁。

(回到頂部)

4.3.不需要用的監聽未移除會發生記憶體洩露

例子1:

// tv.setOnClickListener();//監聽執行完回收物件
//add監聽,放到集合裡面
tv.getViewTreeObserver().addOnWindowFocusChangeListener(new ViewTreeObserver.OnWindowFocusChangeListener() {
	@Override
	public void onWindowFocusChanged(boolean b) {
		//監聽view的載入,view載入出來的時候,計算他的寬高等。

		//計算完後,一定要移除這個監聽
		tv.getViewTreeObserver().removeOnWindowFocusChangeListener(this);
	}
});
複製程式碼

例子2:

SensorManager sensorManager = getSystemService(SENSOR_SERVICE);
Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
sensorManager.registerListener(this,sensor,SensorManager.SENSOR_DELAY_FASTEST);
//不需要用的時候記得移除監聽
sensorManager.unregisterListener(listener);
複製程式碼

(回到頂部)

4.4.資源未關閉引起的記憶體洩露情況

比如:BroadCastReceiver、Cursor、Bitmap、IO流、自定義屬性attribute attr.recycle()回收。
當不需要使用的時候,要記得及時釋放資源。否則就會記憶體洩露。

4.5.無限迴圈動畫

沒有在onDestroy中停止動畫,否則Activity就會變成洩露物件。 比如:輪播圖效果。

(回到頂部)

相關文章