尊重原創做者,轉載請註明出處:java
http://blog.csdn.net/gemmem/article/details/13017999android
此文承接個人另外一篇文章:Android進程的內存管理分析數據庫
首先了解一下dalvik的Garbage Collection:數組
如上圖所示,GC會選擇一些它瞭解還存活的對象做爲內存遍歷的根節點(GC Roots),比方說thread stack中的變量,JNI中的全局變量,zygote中的對象(class loader加載)等,而後開始對heap進行遍歷。到最後,部分沒有直接或者間接引用到GC Roots的就是須要回收的垃圾,會被GC回收掉。以下圖藍色部分。緩存
Java內存泄漏指的是進程中某些對象(垃圾對象)已經沒有使用價值了,可是它們卻能夠直接或間接地引用到gc roots致使沒法被GC回收。無用的對象佔據着內存空間,使得實際可以使用內存變小,形象地說法就是內存泄漏了。下面分析一些可能致使內存泄漏的情景。app
常見的內存泄漏eclipse
一、非靜態內部類的靜態實例容易形成內存泄漏ide
import android.os.Bundle; import android.app.Activity; public class MainActivity extends Activity{ static Demo sInstance = null; @Override public void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if (sInstance == null) { sInstance= new Demo(); } } class Demo{ void voiddoSomething(){ System.out.print("dosth."); } } }
上面的代碼中的sInstance實例類型爲靜態實例,在第一個MainActivity act1實例建立時,sInstance會得到並一直持有act1的引用。當MainAcitivity銷燬後重建,由於sInstance持有act1的引用,因此act1是沒法被GC回收的,進程中會存在2個MainActivity實例(act1和重建後的MainActivity實例),這個act1對象就是一個無用的但一直佔用內存的對象,即沒法回收的垃圾對象。因此,對於lauchMode不是singleInstance的Activity, 應該避免在activity裏面實例化其非靜態內部類的靜態實例。函數
二、activity使用靜態成員工具
private static Drawable sBackground; @Override protected void onCreate(Bundle state) { super.onCreate(state); TextView label = new TextView(this); label.setText("Leaks are bad"); if (sBackground == null) { sBackground = getDrawable(R.drawable.large_bitmap); } label.setBackgroundDrawable(sBackground); setContentView(label); }
因爲用靜態成員sBackground 緩存了drawable對象,因此activity加載速度會加快,可是這樣作是錯誤的。由於在android 2.3系統上,它會致使activity銷燬後沒法被系統回收。
label .setBackgroundDrawable函數調用會將label賦值給sBackground的成員變量mCallback。
上面代碼意味着:sBackground(GC Root)會持有TextView對象,而TextView持有Activity對象。因此致使Activity對象沒法被系統回收。
下面看看android4.0爲了不上述問題所作的改進。
先看看android 2.3的Drawable.Java對setCallback的實現:
public final void setCallback(Callback cb){
mCallback = cb;
}
再看看android 4.0的Drawable.Java對setCallback的實現:
public final void setCallback(Callback cb){
mCallback = newWeakReference<Callback> (cb);
}
在android 2.3中要避免內存泄漏也是能夠作到的, 在activity的onDestroy時調用
sBackgroundDrawable.setCallback(null)。
以上2個例子的內存泄漏都是由於Activity的引用的生命週期超越了activity對象的生命週期。也就是常說的Context泄漏,由於activity就是context。
想要避免context相關的內存泄漏,須要注意如下幾點:
·不要對activity的context長期引用(一個activity的引用的生存週期應該和activity的生命週期相同)
·若是能夠的話,儘可能使用關於application的context來替代和activity相關的context
·若是一個acitivity的非靜態內部類的生命週期不受控制,那麼避免使用它;正確的方法是使用一個靜態的內部類,而且對它的外部類有一WeakReference,就像在ViewRootImpl中內部類W所作的那樣。
三、使用handler時的內存問題
咱們知道,Handler經過發送Message與主線程交互,Message發出以後是存儲在MessageQueue中的,有些Message也不是立刻就被處理的。在Message中存在一個 target,是Handler的一個引用,若是Message在Queue中存在的時間越長,就會致使Handler沒法被回收。若是Handler是非靜態的,則會致使Activity或者Service不會被回收。 因此正確處理Handler等之類的內部類,應該將本身的Handler定義爲靜態內部類。
HandlerThread的使用也須要注意:
當咱們在activity裏面建立了一個HandlerThread,代碼以下:
public classMainActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Thread mThread = newHandlerThread("demo", Process.THREAD_PRIORITY_BACKGROUND); mThread.start(); MyHandler mHandler = new MyHandler( mThread.getLooper( ) ); ……. ……. ……. } @Override public void onDestroy() { super.onDestroy(); } }
這個代碼存在泄漏問題,由於HandlerThread的run方法是一個死循環,它不會本身結束,線程的生命週期超過了activity生命週期,當橫豎屏切換,HandlerThread線程的數量會隨着activity重建次數的增長而增長。
應該在onDestroy時將線程中止掉:mThread.getLooper().quit();
另外,對於不是HandlerThread的線程,也應該確保activity消耗後,線程已經終止,能夠這樣作:在onDestroy時調用mThread.join();
四、註冊某個對象後未反註冊
註冊廣播接收器、註冊觀察者等等,好比:
假設咱們但願在鎖屏界面(LockScreen)中,監聽系統中的電話服務以獲取一些信息(如信號強度等),則能夠在LockScreen中定義一個PhoneStateListener的對象,同時將它註冊到TelephonyManager服務中。對於LockScreen對象,當須要顯示鎖屏界面的時候就會建立一個LockScreen對象,而當鎖屏界面消失的時候LockScreen對象就會被釋放掉。
可是若是在釋放LockScreen對象的時候忘記取消咱們以前註冊的PhoneStateListener對象,則會致使LockScreen沒法被GC回收。若是不斷的使鎖屏界面顯示和消失,則最終會因爲大量的LockScreen對象沒有辦法被回收而引發OutOfMemory,使得system_process進程掛掉。
雖然有些系統程序,它自己好像是能夠自動取消註冊的(固然不及時),可是咱們仍是應該在咱們的程序中明確的取消註冊,程序結束時應該把全部的註冊都取消掉。
五、集合中對象沒清理形成的內存泄露
咱們一般把一些對象的引用加入到了集合中,當咱們不須要該對象時,若是沒有把它的引用從集合中清理掉,這樣這個集合就會愈來愈大。若是這個集合是static的話,那狀況就更嚴重了。
好比某公司的ROM的鎖屏曾經就存在內存泄漏問題:
這個泄漏是由於LockScreen每次顯示時會註冊幾個callback,它們保存在KeyguardUpdateMonitor的ArrayList<InfoCallback>、ArrayList<SimStateCallback>等ArrayList實例中。可是在LockScreen解鎖後,這些callback沒有被remove掉,致使ArrayList不斷增大, callback對象不斷增多。這些callback對象的size並不大,heap增加比較緩慢,須要長時間地使用手機才能出現OOM,因爲鎖屏是駐留在system_server進程裏,因此致使結果是手機重啓。
六、資源對象沒關閉形成的內存泄露
資源性對象好比(Cursor,File文件等)每每都用了一些緩衝,咱們在不使用的時候,應該及時關閉它們,以便它們的緩衝及時回收內存。它們的緩衝不只存在於Java虛擬機內,還存在於Java虛擬機外。若是咱們僅僅是把它的引用設置爲null,而不關閉它們,每每會形成內存泄露。由於有些資源性對象,好比SQLiteCursor(在析構函數finalize(),若是咱們沒有關閉它,它本身會調close()關閉),若是咱們沒有關閉它,系統在回收它時也會關閉它,可是這樣的效率過低了。所以對於資源性對象在不使用的時候,應該當即調用它的close()函數,將其關閉掉,而後再置爲null.在咱們的程序退出時必定要確保咱們的資源性對象已經關閉。
程序中常常會進行查詢數據庫的操做,可是常常會有使用完畢Cursor後沒有關閉的狀況。若是咱們的查詢結果集比較小,對內存的消耗不容易被發現,只有在長時間大量操做的狀況下才會復現內存問題,這樣就會給之後的測試和問題排查帶來困難和風險。
七、一些不良代碼成內存壓力
有些代碼並不形成內存泄露,可是它們或是對沒使用的內存沒進行有效及時的釋放,或是沒有有效的利用已有的對象而是頻繁的申請新內存,對內存的回收和分配形成很大影響的,容易迫使虛擬機不得不給該應用進程分配更多的內存,增長vm的負擔,形成沒必要要的內存開支。
7.1,Bitmap使用不當
第1、及時的銷燬。
雖然,系統可以確認Bitmap分配的內存最終會被銷燬,可是因爲它佔用的內存過多,因此極可能會超過Java堆的限制。所以,在用完Bitmap時,要及時的recycle掉。recycle並不能肯定當即就會將Bitmap釋放掉,可是會給虛擬機一個暗示:「該圖片能夠釋放了」。
第2、設置必定的採樣率。
有時候,咱們要顯示的區域很小,沒有必要將整個圖片都加載出來,而只須要記載一個縮小過的圖片,這時候能夠設置必定的採樣率,那麼就能夠大大減少佔用的內存。以下面的代碼:
private ImageView preview; BitmapFactory.Options options = newBitmapFactory.Options(); options.inSampleSize = 2;//圖片寬高都爲原來的二分之一,即圖片爲原來的四分之一 Bitmap bitmap =BitmapFactory.decodeStream(cr.openInputStream(uri), null, options); preview.setImageBitmap(bitmap);
第3、巧妙的運用軟引用(SoftRefrence)
有些時候,咱們使用Bitmap後沒有保留對它的引用,所以就沒法調用Recycle函數。這時候巧妙的運用軟引用,可使Bitmap在內存快不足時獲得有效的釋放。以下:
SoftReference<Bitmap> bitmap_ref = new SoftReference<Bitmap>(BitmapFactory.decodeStream(inputstream)); …… …… if (bitmap_ref .get() != null) bitmap_ref.get().recycle();
7.2,構造Adapter時,沒有使用緩存的 convertView
以構造ListView的BaseAdapter爲例,在BaseAdapter中提共了方法:
public View getView(intposition, View convertView, ViewGroup parent)
來向ListView提供每個item所須要的view對象。初始時ListView會從BaseAdapter中根據當前的屏幕布局實例化必定數量的view對象,同時ListView會將這些view對象緩存起來。當向上滾動ListView時,原先位於最上面的list item的view對象會被回收,而後被用來構造新出現的最下面的list item。這個構造過程就是由getView()方法完成的,getView()的第二個形參 View convertView就是被緩存起來的list item的view對象(初始化時緩存中沒有view對象則convertView是null)。
由此能夠看出,若是咱們不去使用convertView,而是每次都在getView()中從新實例化一個View對象的話,即浪費時間,也形成內存垃圾,給垃圾回收增長壓力,若是垃圾回收來不及的話,虛擬機將不得不給該應用進程分配更多的內存,形成沒必要要的內存開支。ListView回收list item的view對象的過程能夠查看:
android.widget.AbsListView.Java--> void addScrapView(View scrap) 方法。
Java代碼:
public View getView(int position, View convertView, ViewGroupparent) { View view = newXxx(...); return view; }
修正示例代碼:
Java代碼:
public View getView(intposition, View convertView, ViewGroup parent) { View view = null; if (convertView != null){ view = convertView; populate(view, getItem(position)); } else { view = new Xxx(...); } return view; }
7.三、不要在常常調用的方法中建立對象,尤爲是忌諱在循環中建立對象。能夠適當的使用 hashtable , vector 建立一組對象容器,而後從容器中去取那些對象,而不用每次 new 以後又丟棄。
關於內存泄漏的調試
(1).內存監測工具 DDMS --> Heap
不管怎麼當心,想徹底避免bad code是不可能的,此時就須要一些工具來幫助咱們檢查代碼中是否存在會形成內存泄漏的地方。Android tools中的DDMS就帶有一個很不錯的內存監測工具Heap(這裏我使用eclipse的ADT插件,並以真機爲例,在模擬器中的狀況相似)。用 Heap監測應用進程使用內存狀況的步驟以下:
1. 啓動eclipse後,切換到DDMS透視圖,並確認Devices視圖、Heap視圖都是打開的;
2. 將手機經過USB連接至電腦,連接時須要確認手機是處於「USB調試」模式,而不是做爲「MassStorage」;
3. 連接成功後,在DDMS的Devices視圖中將會顯示手機設備的序列號,以及設備中正在運行的部分進程信息;
4. 點擊選中想要監測的進程,好比system_process進程;
5. 點擊選中Devices視圖界面中最上方一排圖標中的「Update Heap」圖標;
6. 點擊Heap視圖中的「Cause GC」按鈕;
7. 此時在Heap視圖中就會看到當前選中的進程的內存使用量的詳細狀況。
說明:
a) 點擊「Cause GC」按鈕至關於向虛擬機請求了一次gc操做;
b) 當內存使用信息第一次顯示之後,無須再不斷的點擊「CauseGC」,Heap視圖界面會定時刷新,在對應用的不斷的操做過程當中就能夠看到內存使用的變化;
c) 內存使用信息的各項參數根據名稱便可知道其意思,在此再也不贅述。
如何才能知道咱們的程序是否有內存泄漏的可能性呢。這裏須要注意一個值:Heap視圖中部有一個Type叫作dataobject,即數據對象,也就是咱們的程序中大量存在的類類型的對象。在data object一行中有一列是「Total Size」,其值就是當前進程中全部Java數據對象的內存總量,通常狀況下,這個值的大小決定了是否會有內存泄漏。能夠這樣判斷:
a) 不斷的操做當前應用,同時注意觀察data object的Total Size值;
b) 正常狀況下Total Size值都會穩定在一個有限的範圍內,也就是說因爲程序中的的代碼良好,沒有形成對象不被垃圾回收的狀況,因此說雖然咱們不斷的操做會不斷的生成不少對象,而在虛擬機不斷的進行GC的過程當中,這些對象都被回收了,內存佔用量會會落到一個穩定的水平;
c) 反之若是代碼中存在沒有釋放對象引用的狀況,則dataobject的Total Size值在每次GC後不會有明顯的回落,隨着操做次數的增多Total Size的值會愈來愈大,
直到到達一個上限後致使進程OOM被kill掉。
(2).內存分析工具 MAT(Memory Analyzer Tool)
並非全部的內存泄漏均可以用觀察heap size的方法檢測出來,由於有的程序只是泄漏了幾個對象,並且泄漏的對象個數不會隨着程序的運行而增長,這種內存泄漏不會直接致使OOM,可是無用對象沒法回收,無疑是對內存的浪費,會影響到程序的性能,咱們須要使用MAT工具才能發現這種比較隱蔽的內存泄漏。
使用MAT以前有2個概念是要掌握的:Shallowheap和Retained heap。Shallow heap表示對象自己所佔內存大小,一個內存大小100bytes的對象Shallow heap就是100bytes。Retained heap表示經過回收這一個對象總共能回收的內存,比方說一個100bytes的對象還直接或者間接地持有了另外3個100bytes的對象引用,回收這個對象的時候若是另外3個對象沒有其餘引用也能被回收掉的時候,Retained heap就是400bytes。
MAT使用Dominator Tree這樣一種來自圖形理論的概念。
所謂Dominator,就是Flow Graph中從源節點出發到某個節點的的必經節點。那麼根據這個概念咱們能夠從上圖左側的Flow Graph構造出右側的Dominator Tree。這樣一來很容易就看出每一個節點的Retained heap了。Shallow heap和Retained heap在MAT中是很是有用的概念,用於內存泄漏的分析。
咱們作一個Demo。在工程的MainActivity當中加入以下代碼:
publicclassMainActivityextends Activity{ static Leaky leak=null; classLeaky{ voiddoSomething(){ System.out.println("Wheee!!!"); } } @Override publicvoidonCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); if(leak==null){ leak =new Leaky(); } ...
上面這段代碼,對Java熟悉的同窗都應該瞭解非靜態內部類對象默認持有外部類對象引用,而leak做爲靜態變量在非空判斷下只產生了一個對象,所以當旋轉屏幕時生成新的Activity的時候舊的Activity的引用依然被持有,以下圖:
經過觀察旋轉屏幕先後Log中GC的信息也能看出heap的data object分配往上漲了一點,而且在GC執行完heap的分配穩定以後並無降下來,這就是內存泄漏的跡象。
咱們經過MAT來進行分析。先下載MAT,能夠做爲Eclipse插件下載,也能夠做爲RCP應用下載,本質上沒有區別。DDMS中選中應用對應的進程名,點擊Dump HPROF file的按鈕,等一小段時間生成HPROF文件,若是是Eclipse插件的話,Eclipse會爲這個HPROF自動轉化成標準的HPROF並自動打開MAT分析界面。若是是做爲RCP應用的話,須要用sdk目錄tools中的hprof-conv工具來進行轉化,也就是上文說起的命令hprof-convorig.hprof converted.hprof,這種方式保存HPROF文件的位置選擇更爲自主,你也能夠修改Eclipse的設置讓Eclipse提示保存而不是自動打開,在Preferences -> Android -> DDMS中的HPROFAction由Open in Eclipse改成Save todisk。打開MAT,選擇轉化好的HPROF文件,能夠看到Overview的界面以下圖:
中間的餅狀圖就是根據咱們上文所說的Retained heap的概念獲得的內存中一些Retained Size最大的對象。點擊餅狀圖能看到這些對象類型,但對內存泄漏的分析還遠遠不夠。再看下方Action中有Dominator Tree和Histogram的選項,這通常來講是最有用的工具。還記得咱們上文說過的DominatorTree的概念嗎,這就是咱們用來跟蹤內存泄漏的方式。點開Dominator Tree,會看到以Retained heap排序的一系列對象,以下圖:
Resources類型對象因爲通常是系統用於加載資源的,因此Retained heap較大是個比較正常的狀況。但咱們注意到下面的Bitmap類型對象的Retained heap也很大,頗有多是因爲內存泄漏形成的。因此咱們右鍵點擊這行,選擇Path To GC Roots ->exclude weak references,能夠看到下圖的情形:
Bitmap最終被leak引用到,這應該是一種不正常的現象,內存泄漏極可能就在這裏了。MAT不會告訴哪裏是內存泄漏,須要你自行分析,因爲這是Demo,是咱們特地形成的內存泄漏,所以比較容易就能看出來,真實的應用場景可能須要你仔細的進行分析。
根據咱們上文介紹的Dominator的概念,leak對象是該Bitmap對象的Dominator,應該出如今Dominator Tree視圖裏面,但實際上卻沒有。這是因爲MAT並無對weak references作區別對待,這也是咱們選擇exclude weakreferences的緣由。若是咱們Path To GC Roots ->with all references,咱們能夠看到下圖的情形:
能夠看到還有另一個對象在引用着這個Bitmap對象,瞭解weak references的同窗應該知道GC是如何處理weak references,所以在內存泄漏分析的時候咱們能夠把weak references排除掉。
有些同窗可能但願根據某種類型的對象個數來分析內存泄漏。咱們在Overview視圖中選擇Actions -> Histogram,能夠看到相似下圖的情形:
上圖展現了內存中各類類型的對象個數和Shallow heap,咱們看到byte[]佔用Shallow heap最多,那是由於Honeycomb以後Bitmap Pixel Data的內存分配在Dalvik heap中。右鍵選中byte[]數組,選擇List Objects -> with incomingreferences,能夠看到byte[]具體的對象列表:
咱們發現第二個byte[]的Retained heap較大,內存泄漏的可能性較大,所以右鍵選中這行,Path To GC Roots -> exclude weak references,一樣能夠看到上文所提到的狀況,咱們的Bitmap對象被leak所引用到,這裏存在着內存泄漏。
在Histogram視圖中第一行<Regex>中輸入com.example.android.hcgallery,過濾出咱們本身應用中的類型,以下圖:
咱們發現本應該只有一個MainActivity如今卻有兩個,顯然不正常。右鍵選擇List Objects-> with incoming references,能夠看到這兩個具體的MainActivity對象。右鍵選中Retained heap較大的MainActivity,Path To GC Roots -> exclude weak references,再一次可疑對象又指向了leak對象。
以上是MAT一些基本的用法,若是你感興趣,能夠自行深刻的去了解MAT的其餘功能。