1.Java記憶體分配策略
Java 程式執行時的記憶體分配策略有三種:靜態分配、棧式分配和堆式分配。對應的儲存區域如下:
- 靜態儲存區(方法區):主要存放靜態資料、全域性 static 資料和常量。這塊記憶體在程式編譯時就已經分配好,並且在程式整個執行期間都存在。
- 棧區 :方法體內的區域性變數都在棧上建立,並在方法執行結束時這些區域性變數所持有的記憶體將會自動被釋放。
- 堆區 : 又稱動態記憶體分配,通常就是指在程式執行時直接 new 出來的記憶體。這部分記憶體在不使用時將會由 Java 垃圾回收器來負責回收。
2.堆與棧的區別
棧記憶體:在方法體內定義的區域性變數(一些基本型別的變數和物件的引用變數)都是在方法的棧記憶體中分配的。當在一段方法塊中定義一個變數時,Java 就會在棧中為該變數分配記憶體空間,當超過該變數的作用域後,分配給它的記憶體空間也將被釋放掉,該記憶體空間可以被重新使用。
堆記憶體:用來存放所有由 new 建立的物件(包括該物件其中的所有成員變數)和陣列。在堆中分配的記憶體,將由 Java 垃圾回收器來自動管理。在堆中產生了一個陣列或者物件後,還可以在棧中定義一個特殊的變數,這個變數的取值等於陣列或者物件在堆記憶體中的首地址,這個特殊的變數就是我們上面說的引用變數。我們可以通過這個引用變數來訪問堆中的物件或者陣列。
例子:
public class A {
int a = 0;
B b = new B();
public void test(){
int a1 = 1;
B b1 = new B();
}
}
A object = new A();複製程式碼
- A類內的區域性變數都存在於棧中,包括基本資料型別a1和引用變數b1,b1指向的B物件實體存在於堆中
- 引用變數object存在於棧中,而object指向的物件實體存在於堆中,包括這個物件的所有成員變數a和b,而引用變數b指向的B類物件實體存在於堆中
3.Java管理記憶體的機制
Java的記憶體管理就是物件的分配和釋放問題。記憶體的分配是由程式設計師來完成,記憶體的釋放由GC(垃圾回收機制)完成。GC 為了能夠正確釋放物件,必須監控每一個物件的執行狀態,包括物件的申請、引用、被引用、賦值等。這是Java程式執行較慢的原因之一。
釋放物件的原則:
該物件不再被引用。
GC的工作原理:
將物件考慮為有向圖的頂點,將引用關係考慮為有向圖的有向邊,有向邊從引用者指向被引物件。另外,每個執行緒物件可以作為一個圖的起始頂點,例如大多程式從 main 程式開始執行,那麼該圖就是以 main 程式為頂點開始的一棵根樹。在有向圖中,根頂點可達的物件都是有效物件,GC將不回收這些物件。如果某個物件與這個根頂點不可達,那麼我們認為這個物件不再被引用,可以被 GC 回收。
下面舉一個例子說明如何用有向圖表示記憶體管理。對於程式的每一個時刻,我們都有一個有向圖表示JVM的記憶體分配情況。以下右圖,就是左邊程式執行到第6行的示意圖。
另外,Java使用有向圖的方式進行記憶體管理,可以消除引用迴圈的問題,例如有三個物件相互引用,但只要它們和根程式不可達,那麼GC也是可以回收它們的。當然,除了有向圖的方式,還有一些別的記憶體管理技術,不同的記憶體管理技術各有優缺點,在這裡就不詳細展開了。
4.Java中的記憶體洩漏
如果一個物件滿足以下兩個條件:
(1)這些物件是可達的,即在有向圖中,存在通路可以與其相連
(2)這些物件是無用的,即程式以後不會再使用這些物件
就可以判定為Java中的記憶體洩漏,這些物件不會被GC所回收,繼續佔用著記憶體。
在C++中,記憶體洩漏的範圍更大一些。有些物件被分配了記憶體空間,然後卻不可達,由於C++中沒有GC,這些記憶體將永遠收不回來。在Java中,這些不可達的物件都由GC負責回收,因此程式設計師不需要考慮這部分的記憶體洩漏。
5.Android中常見的記憶體洩漏
(1)單例造成的記憶體洩漏
這是一個普通的單例模式,當建立這個單例的時候,由於需要傳入一個Context,所以這個Context的生命週期的長短至關重要:
1.如果此時傳入的是 Application 的 Context,因為 Application 的生命週期就是整個應用的生命週期,所以沒有任何問題。
2.如果此時傳入的是 Activity 的 Context,當這個 Context 所對應的 Activity 退出時,由於該 Context 的引用被單例物件所持有,其生命週期等於整個應用程式的生命週期,所以當前 Activity 退出時它的記憶體並不會被回收,這就造成洩漏了。
當然,Application 的 context 不是萬能的,所以也不能隨便亂用,例如Dialog必須使用 Activity 的 Context。對於這部分有興趣的讀者可以自行搜尋相關資料。
(2)非靜態內部類建立靜態例項造成的記憶體洩漏
public class MainActivity extends AppCompatActivity {
private static TestResource mResource = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if(mManager == null){
mManager = new TestResource();
}//...
}
class TestResource {//...
}
}複製程式碼
非靜態內部類預設會持有外部類的引用,而該非靜態內部類又建立了一個靜態的例項,該例項的生命週期和應用的一樣長,這就導致了該靜態例項一直會持有該Activity的引用,導致Activity的記憶體資源不能正常回收。
(3)匿名內部類造成的記憶體洩漏
匿名內部類預設也會持有外部類的引用。
如果在Activity/Fragment中使用了匿名類,並被非同步執行緒持有,如果沒有任何措施這樣一定會導致洩漏。ref1和ref2的區別是,ref2使用了匿名內部類。我們來看看執行時這兩個引用的記憶體:
可以看到,ref1沒什麼特別的。但ref2這個匿名類的實現物件裡面多了一個引用:
this$0這個引用指向MainActivity.this,也就是說當前的MainActivity例項會被ref2持有,如果將這個引用再傳入一個非同步執行緒,此執行緒和此Acitivity生命週期不一致的時候,就會造成Activity的洩漏。
例子:Handler造成的記憶體洩漏
在該MainActivity 中宣告瞭一個延遲10分鐘執行的訊息 Message,mHandler 將其 push 進了訊息佇列 MessageQueue 裡。當該 Activity 被 finish() 掉時,延遲執行任務的 Message 還會繼續存在於主執行緒中,它持有該 Activity 的 Handler 引用,然後又因 為 Handler 為匿名內部類,它會持有外部類的引用(在這裡就是指MainActivity),所以此時 finish() 掉的 Activity 就不會被回收了,從而造成記憶體洩漏。
修復方法:在 Activity 中避免使用非靜態內部類或匿名內部類,比如將 Handler 宣告為靜態的,則其存活期跟 Activity 的生命週期就無關了。如果需要用到Activity,就通過弱引用的方式引入 Activity,避免直接將 Activity 作為 context 傳進去。另外, Looper 執行緒的訊息佇列中還是可能會有待處理的訊息,所以我們在 Activity 的 Destroy 時或者 Stop 時應該移除訊息佇列 MessageQueue 中的訊息。見下面程式碼:
(4)資源未關閉造成的記憶體洩漏
對於使用了BraodcastReceiver,ContentObserver,File, Cursor,Stream,Bitmap等資源的使用,應該在Activity銷燬時及時關閉或者登出,否則這些資源將不會被回收,造成記憶體洩漏。
(5)一些不良程式碼造成的記憶體壓力
有些程式碼並不造成記憶體洩漏,但是它們,或是對沒使用的記憶體沒進行有效及時的釋放,或是沒有有效的利用已有的物件而是頻繁的申請新記憶體。比如,Adapter裡沒有複用convertView等。
6.Android中記憶體洩漏的排查與分析
(1)利用Android Studio的Memory Monitor來檢測記憶體情況
先來看一下Android Studio 的 Memory Monitor介面:
最原始的記憶體洩漏排查方式如下:
重複多次操作關鍵的可疑的路徑,從記憶體監控工具中觀察記憶體曲線,看是否存在不斷上升的趨勢,且退出一個介面後,程式記憶體遲遲不降低的話,可能就發生了嚴重的記憶體洩漏。
這種方式可以發現最基本,也是最明顯的記憶體洩漏問題,對使用者價值最大,操作難度小,價效比極高。
下面就開始用一個簡單的例子來說明一下如何排查記憶體洩漏。
首先,建立了一個TestActivity類,裡面的測試程式碼如下:
@Override
protected void processBiz() {
mHandler = new Handler();
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
MLog.d("------postDelayed------");
}
}, 800000L);
}複製程式碼
執行專案,並執行以下操作:進入TestActivity,然後退出,再重新進入,如此操作幾次後,最後最終退出TestActivity。這時發現,記憶體持續增高,如圖所示:
好了,這時我們可以假設,這裡可能出現了記憶體洩漏的情況。那麼,如何繼續定位到記憶體洩漏的地址呢?這時候就得點選“Dump java heap”按鈕來收集具體的資訊了。
(2)使用Android Studio生成Java Heap檔案來分析記憶體情況
注意,在點選 Dump java heap 按鈕之前,一定要先點選Initate GC按鈕強制GC,建議點選後等待幾秒後再次點選,嘗試多次,讓GC更加充分。然後再點選Dump Java Heap按鈕。
這時候會生成一個Java heap檔案並在新的視窗開啟:
這時候,點選右上角的“Analyzer Task”,再點選出現的綠色按鈕,讓Android studio幫我們自動分析出有可能潛在的記憶體洩漏的地方:
如上圖所示,Android studio提示有3個TestActivity物件可能出現了記憶體洩漏。而且左邊的Reference Tree(引用樹),也大概列出了該實體類被引用的路徑。如果是一些比較簡單的記憶體洩漏情況,僅僅看這裡就大概能猜到是哪裡導致了記憶體洩漏。
但如果是比較複雜的情況,還是推薦使用MAT工具(Memory Analyzer)來繼續分析比較好。
(3)使用Memory Analyzer(MAT)來分析記憶體洩漏
MAT是Eclipse出品的一個外掛,當然也有獨立的版本。下載連結:MAT下載地址
在這裡先提醒一下:MAT並不會準確地告訴我們哪裡發生了記憶體洩漏,而是會提供一大堆的資料和線索,我們需要根據自己的實際程式碼和業務邏輯去分析這些資料,判斷到底是不是真的發生了記憶體洩漏。
MAT支援對標準格式的hprof檔案進行記憶體分析,所以,我們要先在Android Studio裡先把Java heap檔案轉成標準格式的hprof檔案,具體步驟如下:
點選左側的capture,選擇對應的檔案,並右鍵選擇“Export to standard .hprof”匯出標準的hprof檔案:
匯出標準的hprof檔案後,在MAT工具裡匯入,則看到以下介面:
MAT中提供了非常多的功能,這裡我們只要學習幾個最常用的就可以了。上圖那個餅狀圖展示了最大的幾個物件所佔記憶體的比例,這張圖中提供的內容並不多,我們可以忽略它。在這個餅狀圖的下方就有幾個非常有用的工具:
Histogram:直方圖,可以列出記憶體中每個物件的名字、數量以及大小。
Dominator Tree:會將所有記憶體中的物件按大小進行排序,並且我們可以分析物件之間的引用結構。
1)Dominator Tree
從上圖可以看到右邊存在著3個引數。Retained Heap表示這個物件以及它所持有的其它引用(包括直接和間接)所佔的總記憶體,因此從上圖中看,前兩行的Retained Heap是最大的,分析記憶體洩漏時,記憶體最大的物件也是最應該去懷疑的。
另外大家應該可以注意到,在每一行的最左邊都有一個檔案型的圖示,這些圖示有的左下角帶有一個紅色的點,有的則沒有。帶有紅點的物件就表示是可以被GC Roots訪問到的,
可以被GC Root訪問到的物件都是無法被回收的。那麼這就可以說明所有帶紅色的物件都是洩漏的物件嗎?當然不是,因為有些物件系統需要一直使用,本來就不應該被回收。
如果發現有的物件右邊有寫著System Class,那麼說明這是一個由系統管理的物件,並不是由我們自己建立並導致記憶體洩漏的物件。
根據我們在Android studio的Java heap檔案的提示,TestActivity物件有可能發生了記憶體洩漏,於是我們直接在上面搜TestActivity(這個搜尋功能也是很強大的):
左邊的inspector可以檢視物件內部的各種資訊:
當然,如果你覺得按照預設的排序方式來檢視不方便,你可以自行設定排序的方式:
- Group by class
- Group by class loader
- Group by package
從上圖可以看出,我們搜出了3個TestActivity的物件,一般在退出某個activity後,就結束了一個activity的生命週期,應該會被GC正常回收才對的。通常情況下,一個activity應該只有1個例項物件,但是現在居然有3個TestActivity物件存在,說明之前的操作,產生了3個TestActivity物件,並且無法被系統回收掉。
接下來繼續檢視引用路徑。
對著TestActivity物件點選右鍵 -> Merge Shortest Paths to GC Roots(當然,這裡也可以選擇Path To GC Roots) -> exclude all phantom/weak/soft etc. references
為什麼選擇exclude all phantom/weak/soft etc. references呢?因為弱引用等是不會阻止物件被垃圾回收器回收的,所以我們這裡直接把它排除掉
接下來就能看到引用路徑關係圖了:
從上圖可以看出,TestActivity是被this$0所引用的,它實際上是匿名類對當前類的引用。this$0又被callback所引用,接著它又被Message中一串的next所引用...到這裡,我們就已經分析出記憶體洩漏的原因了,接下來就是去改善存在問題的程式碼了。
2)Histogram
這裡是把當前應用程式中所有的物件的名字、數量和大小全部都列出來了,那麼Shallow Heap又是什麼意思呢?就是當前物件自己所佔記憶體的大小,不包含引用關係的。
上圖當中,byte[]物件的Shallow Heap最高,說明我們應用程式中用了很多byte[]型別的資料,比如說圖片。可以通過右鍵 -> List objects -> with incoming references來檢視具體是誰在使用這些byte[]。
當然,除了一般的物件,我們還可以專門檢視執行緒物件的資訊:
Histogram中是可以顯示物件的數量的,比如說我們現在懷疑TestActivity中有可能存在記憶體洩漏,就可以在第一行的正規表示式框中搜尋“TestActivity”,如下所示:
接下來對著TestActivity右鍵 -> List objects -> with outgoing references檢視具體TestActivity例項
注:
List objects -> with outgoing
references :表示該物件的出節點(被該物件引用的物件)List objects -> with incoming references:表示該物件的入節點(引用到該物件的物件)
如果想要檢視記憶體洩漏的具體原因,可以對著任意一個TestActivity的例項右鍵 -> Merge Shortest Paths to GC Roots(當然,這裡也可以選擇Path To GC Roots) ->
exclude all phantom/weak/soft etc. references,如下圖所示:從這裡可以看出,Histogram和Dominator Tree兩種方式下操作都是差不多的,只是兩種統計圖展示的側重點不太一樣,實際操作中,根據需求選擇不同的方式即可。
3)兩個hprof檔案的對比
為了排查記憶體洩漏,經常會需要做一些前後的對比。下面簡單說一下兩種對比方式:
1.直接對比
工具欄最右邊有個“Compare to another heap dump”的按鈕,只要點選,就可以生成對比後的結果。(注意,要先在MAT中開啟要對比的hprof檔案,才能選擇對比的檔案):
2.新增到campare basket裡對比
在window選單下面選擇compare basket:
在檔案的Histogram view模式下,在navigation history下選擇add to compare basket:
然後就可以通過 Compare Tables 來進行對比了:
7.總結
最後,還是要再次提醒一下,工具是死的,人是活的,MAT也沒有辦法保證一定可以將記憶體洩漏的原因找出來,還是需要我們對程式的程式碼有足夠多的瞭解,知道有哪些物件是存活的,以及它們存活的原因,然後再結合MAT給出的資料來進行具體的分析,這樣才有可能把一些隱藏得很深的問題原因給找出來。
文章同步釋出在 zhuanlan.zhihu.com/p/27593816
參考資料:
(1)Java的記憶體洩漏
阿里雲最近開始發放代金券了,新老使用者均可免費獲取,
新註冊使用者可以獲得1000元代金券,老使用者可以獲得270元代金券,建議大家都領取一份,反正是免費領的,說不定以後需要呢?
阿里雲代金券 領取
https://promotion.aliyun.com/ntms/yunparter/invite.html?userCode=qiziieg4
熱門活動 高效能雲伺服器特惠 助力企業上雲 效能級主機2-5折
https://promotion.aliyun.com/ntms/act/enterprise-discount.html?userCode=qiziieg4