標籤:
不少人認為JAVA程式,因為有記憶體回收機制,應該沒有記憶體泄露。其實如果我們一個程式中,已經不再使用某個對象,但是因為仍然有引用指向它,記憶體回收行程就無法回收它,當然該對象佔用的記憶體就無法被使用,這就造成了記憶體泄露。如果我們的java運行很久,而這種記憶體泄露不斷的發生,最後就沒記憶體可用了。當然java的,記憶體流失和C/C++是不一樣的。如果java程式完全結束後,它所有的對象就都不可達了,系統就可以對他們進行記憶體回收,它的記憶體泄露僅僅限於它本身,而不會影響整個系統的。C/C++的記憶體泄露就比較糟糕了,它的記憶體泄露是系統級,即使該C/C++程式退出,它的泄露的記憶體也無法被系統回收,永遠不可用了,除非重啟機器。
Android的一個應用程式的記憶體泄露對別的應用程式影響不大。為了能夠使得Android應用程式安全且快速的運行,Android的每個應用程式都會使用一個專有的Dalvik虛擬機器執行個體來運行,它是由Zygote服務進程孵化出來的,也就是說每個應用程式都是在屬於自己的進程中啟動並執行。Android為不同類型的進程分配了不同的記憶體使用量上限,如果程式在運行過程中出現了記憶體流失的而造成應用進程使用的記憶體超過了這個上限,則會被系統視為記憶體流失,從而被kill掉,這使得僅僅自己的進程被kill掉,而不會影響其他進程(如果是system_process等系統進程出問題的話,則會引起系統重啟)。
一、引用沒釋放造成的記憶體泄露
1.1註冊沒取消造成的記憶體泄露
這種Android的記憶體泄露比純java的記憶體泄露還要嚴重,因為其他一些Android程式可能引用我們的Anroid程式的對象(比如註冊機制)。即使我們的Android程式已經結束了,但是別的引用程式仍然還有對我們的Android程式的某個對象的引用,泄露的記憶體依然不能被記憶體回收。
比如樣本1:
假設我們希望在鎖定畫面(LockScreen)中,監聽系統中的電話語音以擷取一些資訊(如訊號強度等),則可以在LockScreen中定義一個PhoneStateListener的對象,同時將它註冊到TelephonyManager服務中。對於LockScreen對象,當需要顯示鎖定畫面的時候就會建立一個LockScreen對象,而當鎖定畫面消失的時候LockScreen對象就會被釋放掉。
但是如果在釋放LockScreen對象的時候忘記取消我們之前註冊的PhoneStateListener對象,則會導致LockScreen無法被記憶體回收。如果不斷的使鎖定畫面顯示和消失,則最終會由於大量的LockScreen對象沒有辦法被回收而引起OutOfMemory,使得system_process進程掛掉。
雖然有些系統程式,它本身好像是可以自動取消註冊的(當然不及時),但是我們還是應該在我們的程式中明確的取消註冊,程式結束時應該把所有的註冊都取消掉。
1.2集合中對象沒清理造成的記憶體泄露
我們通常把一些對象的引用加入到了集合中,當我們不需要該對象時,並沒有把它的引用從集合中清理掉,這樣這個集合就會越來越大。如果這個集合是static的話,那情況就更嚴重了。
二、資來源物件沒關閉造成的記憶體泄露
資源性對象比如(Cursor,File檔案等)往往都用了一些緩衝,我們在不使用的時候,應該及時關閉它們,以便它們的緩衝及時回收記憶體。它們的緩衝不僅存在於java虛擬機器內,還存在於java虛擬機器外。如果我們僅僅是把它的引用設定為null,而不關閉它們,往往會造成記憶體泄露。因為有些資源性對象,比如SQLiteCursor(在解構函式finalize(),如果我們沒有關閉它,它自己會調close()關閉),如果我們沒有關閉它,系統在回收它時也會關閉它,但是這樣的效率太低了。因此對於資源性對象在不使用的時候,應該調用它的close()函數,將其關閉掉,然後才置為null.在我們的程式退出時一定要確保我們的資源性對象已經關閉。
程式中經常會進行查詢資料庫的操作,但是經常會有使用完畢Cursor後沒有關閉的情況。如果我們的查詢結果集比較小,對記憶體的消耗不容易被發現,只有在常時間大量操作的情況下才會複現記憶體問題,這樣就會給以後的測試和問題排查帶來困難和風險。
三、一些不良代碼成記憶體壓力
有些代碼並不造成記憶體泄露,但是它們,或是對沒使用的記憶體沒進行有效及時的釋放,或是沒有有效利用已有的對象而是頻繁的申請新記憶體,對記憶體的回收和分配造成很大影響的,容易迫使虛擬機器不得不給該應用進程分配更多的記憶體,造成不必要的記憶體開支。
3.1,Bitmap沒調用recycle()
Bitmap對象在不使用時,我們應該先調用recycle()釋放記憶體,然後才它設定為null.雖然recycle()從源碼上看,調用它應該能立即釋放Bitmap的主要記憶體,但是測試結果顯示它並沒能立即釋放記憶體。但是我它應該還是能大大的加速Bitmap的主要記憶體的釋放。
3.2,構造Adapter時,沒有使用緩衝的 convertView
以構造ListView的BaseAdapter為例,在BaseAdapter中提共了方法:
public View getView(int position, 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對象的過程可以查看:
view plaincopy to clipboardprint?
android.widget.AbsListView.java --> void addScrapView(View scrap) 方法。
範例程式碼:
1 public View getView(int position, View convertView, ViewGroup parent) { 2 3 View view = new Xxx(...); 4 5 ... ... 6 7 return view; 8 9 }
修正範例程式碼:
Android記憶體管理
1 public View getView(int position, View convertView, ViewGroup parent) { 2 3 View view = null; 4 5 if (convertView != null) { 6 7 view = convertView; 8 9 populate(view, getItem(position)); 10 11 ... 12 13 } else { 14 15 view = new Xxx(...); 16 17 ... 18 19 } 20 21 return view; 22 23 }
概述:
在android的開發中,要時刻主要記憶體的分配和記憶體回收,因為系統為每一個dalvik虛擬機器分配的記憶體是有限的,在google的G1中,分配的最大堆大小隻有16M,後來的機器一般都為24M,實在是少的可憐。這樣就需要我們在開發過程中要時刻注意。不要因為自己的代碼問題而造成OOM錯誤。
JAVA的記憶體管理:
大家都知道,android應用程式層是由java開發的,android的davlik虛擬機器與jvm也類似,只不過它是基於寄存器的。因此要瞭解android的記憶體管理就必須得瞭解java的記憶體配置和記憶體回收機制。
在java中,是通過new關鍵字來為對象分配記憶體的,而記憶體的釋放是由垃圾收集器(GC)來回收的,工程師在開發的過程中,不需要顯式的去管理記憶體。但是這樣有可能在不知不覺中就會浪費了很多記憶體,最終導致java虛擬機器花費很多時間去進行記憶體回收,更嚴重的是造成JVM的OOM。因此,java工程師還是有必要瞭解JAVA的記憶體配置和記憶體回收機制。
記憶體結構file:///C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp/ksohtml/wps_clip_image-6926.png上面這張圖是JVM的結構圖,它主要四個部分組成:Class Loader子系統和執行引擎,運行時方法區和本地方法區,我們主要來看下RUNTIME DATA AREA區,也就是我們常說的JVM記憶體。可以看出,RUNTIMEDATA AREA區主要由5個部分組成:· Method Area:被裝載的class的元資訊儲存在Method Area中,它是線程共用的· Heap(堆):一個java虛擬機器執行個體中只存在一個堆空間,存放一些對象資訊,它是線程共用的· Java棧: java虛擬機器直接對java棧進行兩種操作,以幀為單位的壓棧和出棧(非線程共用)· 程式計數器(非線程共用)· 本地方法棧(非線程共用) JVM的記憶體回收(GC)file:///C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp/ksohtml/wps_clip_image-12485.pngJVM的垃圾原理是這樣的,它把對象分為年輕代(Young)、年老代(Tenured)、持久代(Perm),對不同生命週期的對象使用不同的記憶體回收演算法。· 年輕代(Young)年輕代分為三個區,一個eden區,兩個Survivor區。程式中產生的大部分新的對象都在Eden區中,當Eden區滿時,還存活的對象將被複製到其中一個Survivor區,當此Survivor區的對象佔用空間滿了時,此區存活的對象又被複製到另外一個Survivor區,當這個Survivor區也滿了的時候,從第一個Survivor區複製過來的並且此時還存活的對象,將被複製到年老代。· 年老代(Tenured)年老代存放的是上面年輕代複製過來的對象,也就是在年輕代中還存活的對象,並且區滿了複製過來的。一般來說,年老代中的對象生命週期都比較長。· 持久代(Perm)用於存放靜態類和方法,持久代對記憶體回收沒有顯著的影響。Android中記憶體泄露監測在瞭解了JVM的記憶體管理後,我們再回過頭來看看,在android中應該怎樣來監測記憶體,從而看在應用中是否存在記憶體配置和記憶體回收問題而造成記憶體泄露情況。在android中,有一個相對來說還不錯的工具,可以用來監測記憶體是否存在泄露情況:DDMS—Heapfile:///C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp/ksohtml/wps_clip_image-22715.png使用方法比較簡單:· 選擇DDMS視圖,並開啟Devices視圖和Heap視圖· 點擊選擇要監控的進程,比如:中我選擇的是system_process· 選中Devices視圖介面上的"update heap" 表徵圖· 點擊Heap視圖中的"Cause GC" 按鈕(相當於向虛擬機器發送了一次GC請求的操作)在Heap視圖中選擇想要監控的Type,一般我們會觀察dataobject的 total size的變化,正常情況下total size的值會穩定在一個有限的範圍內,也就說程式中的代碼良好,沒有造成程式中的對象不被回收的情況。如果代碼中存在沒有釋放對象引用的情況,那麼data object的total size在每次GC之後都不會有明顯的回落,隨著操作次數的增加而total size也在不斷的增加。(說明:選擇好data object後,不斷的操作應用,這樣才可以看出total size的變化)。如果totalsize確實是在不斷增加而沒有回落,說明程式中有沒有被釋放的資源引用。那麼我們應該怎麼來定位呢?Android中記憶體泄露定位Mat(memory analyzer tools)是我們常用的用來定位記憶體泄露的工具,如果你使用ADT,並且安裝了MAT的eclipse外掛程式,你需要做的是進入DDMS視圖的Devices視圖:file:///C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp/ksohtml/wps_clip_image-2165.png點擊"dump HPROF file"按鈕,然後使用MAT分析下載下來的檔案。file:///C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp/ksohtml/wps_clip_image-6565.png |
下面列出了存在的問題,點擊detail進去,會列出詳細的,可能會存在問題的代碼:
file:///C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp/ksohtml/wps_clip_image-32625.png
file:///C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp/ksohtml/wps_clip_image-21158.png
關於MAT的使用可以參考:http://www.blogjava.net/rosen/ ... .html
這位兄弟寫的比較詳細。
總結
不管是java還是android,都應該瞭解記憶體配置和記憶體回收機制,工程師要做到寫的代碼中沒有bad code很難,關鍵是在出現問題的時候該怎麼去排查Android記憶體最佳化
一、 Android的記憶體機制
Android的程式由Java語言編寫,所以Android的記憶體管理與Java的記憶體管理相似。程式員通過new為對象分配記憶體,所有對象在java堆內分配空間;然而對象的釋放是由記憶體回收行程來完成的。C/C++中的記憶體機制是“誰汙染,誰治理”,java的就比較人性化了,給我們請了一個專門的清潔工(GC)。
那麼GC怎麼能夠確認某一個對象是不是已經被廢棄了呢?Java採用了有向圖的原理。Java將參考關聯性考慮為圖的有向邊,有向邊從引用者指向引用對象。線程對象可以作為有向圖的起始頂點,該圖就是從起始頂點開始的一棵樹,根頂點可以到達的對象都是有效對象,GC不會回收這些對象。如果某個對象 (連通子圖)與這個根頂點不可達(注意,該圖為有向圖),那麼我們認為這個(這些)對象不再被引用,可以被GC回收。
二、Android的記憶體溢出
Android的記憶體溢出是如何發生的?
Android的虛擬機器是基於寄存器的Dalvik,它的最大堆大小一般是16M,有的機器為24M。因此我們所能利用的記憶體空間是有限的。如果我們的記憶體佔用超過了一定的水平就會出現OutOfMemory的錯誤。
為什麼會出現記憶體不夠用的情況呢?我想原因主要有兩個:
由於我們程式的失誤,長期保持某些資源(如Context)的引用,造成記憶體泄露,資源造成得不到釋放。
儲存了多個耗用記憶體過大的對象(如Bitmap),造成記憶體超出限制。
三、萬惡的static
static是Java中的一個關鍵字,當用它來修飾成員變數時,那麼該變數就屬於該類,而不是該類的執行個體。所以用static修飾的變數,它的生命週期是很長的,如果用它來引用一些資源耗費過多的執行個體(Context的情況最多),這時就要謹慎對待了。
1 public class ClassName { 2 3 private static Context mContext; 4 5 //省略 6 7 }
以上的代碼是很危險的,如果將Activity賦值到麼mContext的話。那麼即使該Activity已經onDestroy,但是由於仍有對象儲存它的引用,因此該Activity依然不會被釋放。
我們舉Android文檔中的一個例子。
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是一個靜態變數,但是我們發現,我們並沒有顯式的儲存Contex的引用,但是,當Drawable與View串連之後,Drawable就將View設定為一個回調,由於View中是包含Context的引用的,所以,實際上我們依然儲存了Context的引用。這個引用鏈如下:
Drawable->TextView->Context
所以,最終該Context也沒有得到釋放,發生了記憶體泄露。
如何才能有效避免這種引用的發生呢?
應該盡量避免static成員變數引用資源耗費過多的執行個體,比如Context。
Context盡量使用Application Context,因為Application的Context的生命週期比較長,引用它不會出現記憶體泄露的問題。
使用WeakReference代替強引用。比如可以使用WeakReference<Context> mContextRef;
該部分的詳細內容也可以參考Android文檔中Article部分。
四、都是線程惹的禍
線程也是造成記憶體泄露的一個重要的源頭。線程產生記憶體泄露的主要原因在於線程生命週期的不可控。我們來考慮下面一段代碼。
1 public class MyActivity extends Activity { 2 3 @Override 4 5 public void onCreate(Bundle savedInstanceState) { 6 7 super.onCreate(savedInstanceState); 8 9 setContentView(R.layout.main); 10 11 new MyThread().start(); 12 13 } 14 15 16 private class MyThread extends Thread{ 17 18 @Override 19 20 public void run() { 21 22 super.run(); 23 24 //do somthing 25 26 } 27 28 } 29 30 }
這段代碼很平常也很簡單,是我們經常使用的形式。我們思考一個問題:假設MyThread的run函數是一個很費時的操作,當我們開啟該線程後,將裝置的橫屏變為了豎屏,一般情況下當螢幕轉換時會重新建立Activity,按照我們的想法,老的Activity應該會被銷毀才對,然而事實上並非如此。
由於我們的線程是Activity的內部類,所以MyThread中儲存了Activity的一個引用,當MyThread的run函數沒有結束時,MyThread是不會被銷毀的,因此它所引用的老的Activity也不會被銷毀,因此就出現了記憶體泄露的問題。
file:///C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp/ksohtml/wps_clip_image-6439.png
有些人喜歡用Android提供的AsyncTask,但事實上AsyncTask的問題更加嚴重,Thread只有在run函數不結束時才出現這種記憶體泄露問題,然而AsyncTask內部的實現機制是運用了ThreadPoolExcutor,該類產生的Thread對象的生命週期是不確定的,是應用程式無法控制的,因此如果AsyncTask作為Activity的內部類,就更容易出現記憶體泄露的問題。
這種線程導致的記憶體泄露問題應該如何解決呢?
將線程的內部類,改為靜態內部類。
線上程內部採用弱引用儲存Context引用。
解決的模型如下:
1 public abstract class WeakAsyncTask<Params, Progress, Result, WeakTarget> extends 2 AsyncTask<Params, Progress, Result> { 3 protected WeakReference<WeakTarget> mTarget; 4 5 public WeakAsyncTask(WeakTarget target) { 6 mTarget = new WeakReference<WeakTarget>(target); 7 } 8 9 /** {@inheritDoc} */ 10 @Override 11 protected final void onPreExecute() { 12 final WeakTarget target = mTarget.get(); 13 if (target != null) { 14 this.onPreExecute(target); 15 } 16 } 17 18 /** {@inheritDoc} */ 19 @Override 20 protected final Result doInBackground(Params... params) { 21 final WeakTarget target = mTarget.get(); 22 if (target != null) { 23 return this.doInBackground(target, params); 24 } else { 25 return null; 26 } 27 } 28 29 /** {@inheritDoc} */ 30 @Override 31 protected final void onPostExecute(Result result) { 32 final WeakTarget target = mTarget.get(); 33 if (target != null) { 34 this.onPostExecute(target, result); 35 } 36 } 37 38 protected void onPreExecute(WeakTarget target) { 39 // No default action 40 } 41 42 protected abstract Result doInBackground(WeakTarget target, Params... params); 43 44 protected void onPostExecute(WeakTarget target, Result result) { 45 // No default action 46 } 47 }
事實上,線程的問題並不僅僅在於記憶體泄露,還會帶來一些災難性的問題。由於本文討論的是記憶體問題,所以在此不做討論。
由於51cto不讓我一次傳完,說我的字數太多了,所以分開傳了。
五、超級大胖子Bitmap
可以說出現OutOfMemory問題的絕大多數人,都是因為Bitmap的問題。因為Bitmap佔用的記憶體實在是太多了,它是一個“超級大胖子”,特別是解析度大的圖片,如果要顯示多張那問題就更顯著了。
如何解決Bitmap帶給我們的記憶體問題?
及時的銷毀。
雖然,系統能夠確認Bitmap分配的記憶體最終會被銷毀,但是由於它佔用的記憶體過多,所以很可能會超過java堆的限制。因此,在用完Bitmap時,要及時的recycle掉。recycle並不能確定立即就會將Bitmap釋放掉,但是會給虛擬機器一個暗示:“該圖片可以釋放了”。
設定一定的採樣率。
有時候,我們要顯示的地區很小,沒有必要將整個圖片都載入出來,而只需要記載一個縮小過的圖片,這時候可以設定一定的採樣率,那麼就可以大大減小佔用的記憶體。如下面的代碼:
1 private ImageView preview; 2 3 4 BitmapFactory.Options options = new BitmapFactory.Options(); 5 6 7 options.inSampleSize = 2;//圖片寬高都為原來的二分之一,即圖片為原來的四分之一 8 9 Bitmap bitmap = BitmapFactory.decodeStream(cr.openInputStream(uri), null, options);10 11 12 preview.setImageBitmap(bitmap);
巧妙的運用軟引用(SoftRefrence)
有些時候,我們使用Bitmap後沒有保留對它的引用,因此就無法調用Recycle函數。這時候巧妙的運用軟引用,可以使Bitmap在記憶體快不足時得到有效釋放。如下例:
/**本例子為博主隨手一寫,來說明用法,並未驗證*/
1 private class MyAdapter extends BaseAdapter { 2 3 private ArrayList<SoftReference<Bitmap>> mBitmapRefs = new ArrayList<SoftReference<Bitmap>>(); 4 private ArrayList<Value> mValues; 5 private Context mContext; 6 private LayoutInflater mInflater; 7 8 MyAdapter(Context context, ArrayList<Value> values) { 9 mContext = context; 10 mValues = values; 11 mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 12 } 13 public int getCount() { 14 return mValues.size(); 15 } 16 public Object getItem(int i) { 17 return mValues.get(i); 18 } 19 20 public long getItemId(int i) { 21 return i; 22 } 23 24 public View getView(int i, View view, ViewGroup viewGroup) { 25 View newView = null; 26 if(view != null) { 27 newView = view; 28 } else { 29 newView =(View)mInflater.inflate(R.layout.image_view, false); 30 } 31 32 Bitmap bitmap = BitmapFactory.decodeFile(mValues.get(i).fileName); 33 mBitmapRefs.add(new SoftReference<Bitmap>(bitmap)); //此處加入ArrayList 34 ((ImageView)newView).setImageBitmap(bitmap); 35 36 return newView; 37 } 38 }
六、行蹤詭異的Cursor
Cursor是Android查詢資料後得到的一個管理資料集合的類,正常情況下,如果查詢得到的資料量較小時不會有記憶體問題,而且虛擬機器能夠保證Cusor最終會被釋放掉。
然而如果Cursor的資料量特表大,特別是如果裡面有Blob資訊時,應該保證Cursor佔用的記憶體被及時的釋放掉,而不是等待GC來處理。並且Android明顯是傾向於編程者手動的將Cursor close掉,因為在原始碼中我們發現,如果等到記憶體回收行程來回收時,會給使用者以錯誤提示。
所以我們使用Cursor的方式一般如下:
1 Cursor cursor = null; 2 try { 3 cursor = mContext.getContentResolver().query(uri,null, null,null,null); 4 if(cursor != null) { 5 cursor.moveToFirst(); 6 //do something 7 } 8 } catch (Exception e) { 9 e.printStackTrace(); 10 } finally { 11 if (cursor != null) { 12 cursor.close(); 13 } 14 }
有一種情況下,我們不能直接將Cursor關閉掉,這就是在CursorAdapter中應用的情況,但是注意,CursorAdapter在Acivity結束時並沒有自動的將Cursor關閉掉,因此,你需要在onDestroy函數中,手動關閉。
1 protected void onDestroy() { 2 3 if (mAdapter != null && mAdapter.getCurosr() != null) { 4 5 mAdapter.getCursor().close(); 6 7 } 8 9 super.onDestroy(); 10 11 }
CursorAdapter中的changeCursor函數,會將原來的Cursor釋放掉,並替換為新的Cursor,所以你不用擔心原來的Cursor沒有被關閉。
你可能會想到使用Activity的managedQuery來產生Cursor,這樣Cursor就會與Acitivity的生命週期一致了,多麼完美的解決方案!然而事實上managedQuery也有很大的局限性。
managedQuery產生的Cursor必須確保不會被替換,因為可能很多程式事實上查詢條件都是不確定的,因此我們經常會用新查詢的Cursor來替換掉原先的Cursor。因此這種方法適用範圍也是很小。
七、其它要說的。
其實,要減小記憶體的使用,其實還有很多方法和要求。比如不要使用整張整張的圖,盡量使用9path圖片。Adapter要使用convertView等等,好多細節都可以節省記憶體。這些都需要我們去挖掘,誰叫Android的記憶體不給力來著。
android 開發如何做記憶體最佳化