1.Java內存分配策略
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程序運行較慢的緣由之一。
將對象考慮爲有向圖的頂點,將引用關係考慮爲有向圖的有向邊,有向邊從引用者指向被引對象。另外,每一個線程對象能夠做爲一個圖的起始頂點,例如大多程序從 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的內存資源不能正常回收。php
(3)匿名內部類形成的內存泄漏
匿名內部類默認也會持有外部類的引用。
若是在Activity/Fragment中使用了匿名類,並被異步線程持有,若是沒有任何措施這樣必定會致使泄漏。
ref1和ref2的區別是,ref2使用了匿名內部類。咱們來看看運行時這兩個引用的內存:
能夠看到,ref1沒什麼特別的。但ref2這個匿名類的實現對象裏面多了一個引用:
this$0這個引用指向MainActivity.this,也就是說當前的MainActivity實例會被ref2持有,若是將這個引用再傳入一個異步線程,此線程和此Acitivity生命週期不一致的時候,就會形成Activity的泄漏。
在該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文件,具體步驟以下:html
點擊左側的capture,選擇對應的文件,並右鍵選擇「Export to standard .hprof」導出標準的hprof文件:
導出標準的hprof文件後,在MAT工具裏導入,則看到如下界面:
MAT中提供了很是多的功能,這裏咱們只要學習幾個最經常使用的就能夠了。上圖那個餅狀圖展現了最大的幾個對象所佔內存的比例,這張圖中提供的內容並很少,咱們能夠忽略它。在這個餅狀圖的下方就有幾個很是有用的工具:
Histogram:直方圖,能夠列出內存中每一個對象的名字、數量以及大小。
Dominator Tree:會將全部內存中的對象按大小進行排序,而且咱們能夠分析對象之間的引用結構。
從上圖能夠看到右邊存在着3個參數。Retained Heap表示這個對象以及它所持有的其它引用(包括直接和間接)所佔的總內存,所以從上圖中看,前兩行的Retained Heap是最大的,分析內存泄漏時,內存最大的對象也是最應該去懷疑的。
另外你們應該能夠注意到,在每一行的最左邊都有一個文件型的圖標,這些圖標有的左下角帶有一個紅色的點,有的則沒有。帶有紅點的對象就表示是能夠被GC Roots訪問到的,
能夠被GC Root訪問到的對象都是沒法被回收的。那麼這就能夠說明全部帶紅色的對象都是泄漏的對象嗎?固然不是,由於有些對象系統須要一直使用,原本就不該該被回收。
若是發現有的對象右邊有寫着System Class,那麼說明這是一個由系統管理的對象,並非由咱們本身建立並致使內存泄漏的對象。
根據咱們在Android studio的Java heap文件的提示,TestActivity對象有可能發生了內存泄漏,因而咱們直接在上面搜TestActivity(這個搜索功能也是很強大的):java
左邊的inspector能夠查看對象內部的各類信息:
固然,若是你以爲按照默認的排序方式來查看不方便,你能夠自行設置排序的方式:
從上圖能夠看出,咱們搜出了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所引用...到這裏,咱們就已經分析出內存泄漏的緣由了,接下來就是去改善存在問題的代碼了。
這裏是把當前應用程序中全部的對象的名字、數量和大小所有都列出來了,那麼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文件的對比
爲了排查內存泄漏,常常會須要作一些先後的對比。下面簡單說一下兩種對比方式:
工具欄最右邊有個「Compare to another heap dump」的按鈕,只要點擊,就能夠生成對比後的結果。(注意,要先在MAT中打開要對比的hprof文件,才能選擇對比的文件):
在window菜單下面選擇compare basket:
在文件的Histogram view模式下,在navigation history下選擇add to compare basket:
而後就能夠經過 Compare Tables 來進行對比了:
7.總結
最後,仍是要再次提醒一下,工具是死的,人是活的,MAT也沒有辦法保證必定能夠將內存泄漏的緣由找出來,仍是須要咱們對程序的代碼有足夠多的瞭解,知道有哪些對象是存活的,以及它們存活的緣由,而後再結合MAT給出的數據來進行具體的分析,這樣纔有可能把一些隱藏得很深的問題緣由給找出來。程序員
參考資料:正則表達式
阿里雲最近開始發放代金券了,新老用戶都可免費獲取, 新註冊用戶能夠得到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
數組