Android OOM:記憶體管理分析和記憶體洩露原因總結
一、Android程序的記憶體管理分析
1. 程序的地址空間
在32位作業系統中,程序的地址空間為0到4GB,示意圖如下:
這裡主要說明一下Stack和Heap:
- Stack空間:(進棧和出棧)由作業系統控制,其中主要儲存 函式地址、函式引數、區域性變數 等等。
所以Stack空間不需要很大,一般為幾MB大小。 - Heap空間:使用由程式設計師控制,程式設計師可以使用
malloc、new、free、delete等函式
呼叫來操作這片地址空間。
Heap為程式完成各種複雜任務提供記憶體空間,所以空間比較大,一般為幾百MB到幾GB。
正是因為Heap空間由程式設計師管理,所以容易出現使用不當導致嚴重問題。
2. 程序記憶體空間和RAM之間的關係
- 程序的記憶體空間只是 虛擬記憶體,而程式的執行需要的是實實在在的記憶體,即 實體記憶體(RAM)。
在必要時,作業系統會將程式執行中申請的記憶體(虛擬記憶體)對映到RAM,讓程序能夠使用實體記憶體。 - 另外,RAM的一部分被作業系統留作他用,比如視訊記憶體 等等,記憶體對映和視訊記憶體等都是由作業系統控制,我們也不必過多地關注它,程序所操作的空間都是虛擬地址空間,無法直接操作RAM 。
3. Android中的程序
native程序:採用C/C++實現,不包含dalvik例項的linux程序**,
/system/bin/目錄
下面的程式檔案執行後都是以native程序形式存在的。比如/system/bin/surfaceflinger
/system/bin/rild
、procrank
等就是native程序。java程序:例項化了dalvik虛擬機器例項的linux程序,程序的入口main函式為java函式。 dalvik虛擬機器例項的宿主程序是fork()系統呼叫建立的linux程序,所以每一個Android上的java程序實際上就是一個linux程序,只是程序中多了一個dalvik虛擬機器例項。因此,java程序的記憶體分配比native程序複雜。Android系統中的應用程式基本都是java程序,如
桌面
、電話
、聯絡人
、狀態列
等等。
4. Android中程序的堆記憶體
- heap空間 完全由程式設計師控制,我們使用
malloc
C++ new
和java new
所申請的空間都是heap空間, C/C++申請的記憶體空間在native heap中,而java申請的記憶體空間則在dalvik heap中。
5. Android的 java程式為什麼容易出現OOM
因為Android系統對dalvik的vm heapsize作了硬性限制,當java程序申請的java空間超過閾值時,就會丟擲OOM異常(這個閾值可以是48M、24M、16M等,視機型而定),可以通過
adb shell getprop | grep dalvik.vm.heapgrowthlimit
檢視此值。也就是說,程式發生OMM並不表示RAM不足,而是因為程式申請的java heap物件超過了dalvik vm heapgrowthlimit。也就是說,在RAM充足的情況下,也可能發生OOM。
這樣設計的 目的是為了讓Android系統能同時讓比較多的程序常駐記憶體,這樣程式啟動時就不用每次都重新載入到記憶體,能夠給使用者更快的響應 。
6. Android如何應對RAM不足
java程式發生OMM並不是表示RAM不足,如果RAM真的不足,會發生什麼呢? 這時Android的 memory killer
會起作用,當RAM所剩不多時,memory killer會殺死一些優先順序比較低的程序來釋放實體記憶體,讓高優先順序程式得到更多的記憶體。我們在分析log時,看到的程序被殺的log。
Process com.xxx.xxxx(pid xxxx) has died.
7. 應用程式如何繞過dalvikvm heapsize的限制
對於一些大型的應用程式(比如遊戲),記憶體使用會比較多,很容易超超出vm heapsize的限制,這時怎麼保證程式不會因為OOM而崩潰呢?
建立子程序
- 建立一個新的程序,那麼我們就可以把一些物件分配到新程序的heap上了,從而 達到一個應用程式使用更多的記憶體的目的,當然,建立子程序會增加系統開銷,而且並不是所有應用程式都適合這樣做,視需求而定。
- 建立子程序的方法:使用android:process標籤
使用jni在 native heap 上申請空間(推薦使用)
- 因為 native heap 的增長並不受 dalvik vm heapsize 的限制。
- 只要RAM有剩餘空間,程式設計師可以一直在native heap上申請空間,當然如果 RAM快耗盡,memory killer 會殺程序釋放 RAM。
- 我們在使用一些軟體時,有時候會閃退,就可能是軟體在native層申請了比較多的記憶體導致的。比如 UC web 在瀏覽內容比較多的網頁時可能閃退,原因就是其native heap增長到比較大的值,佔用了大量的 RAM,被memory killer殺掉了。
- 使用視訊記憶體(作業系統預留RAM的一部分作為視訊記憶體)
- 使用
OpenGL textures
等API,texture memory
不受dalvik vm heapsize限制,這個沒實踐過。 - 再比如Android中的
GraphicBufferAllocator
申請的記憶體就是視訊記憶體。
- 使用
8. java程式如何才能建立native物件
必須使用 jni,而且應該用C語言的malloc或者C++的new關鍵字。
例項程式碼如下:
JNIEXPORT void JNICALLJava_com_example_demo_TestMemory_nativeMalloc(JNIEnv *, jobject)
{
void *p= malloc(1024*1024*);
SLOGD("allocate 50M Bytes memory");
if (p !=NULL)
{
//memorywill not used without calling memset()
memset(p,0, 1024*1024*50);
} else SLOGE("mallocfailure.");
...
...
free(p); // free memory
}
或者:
JNIEXPORT voidJNICALL Java_com_example_demo_TestMemory_nativeMalloc(JNIEnv *, jobject)
{
SLOGD("allocate 50M Bytesmemory");
char *p = new char[1024 * 1024 * 50];
if (p != NULL)
{
//memory will not usedwithout calling memset()
memset(p, 1, 1024*1024*50);
} else SLOGE("newobject failure.");
...
...
free(p); //free memory
}
malloc
或者new
申請的記憶體是虛擬記憶體,申請之後不會立即對映到實體記憶體,即不會佔用RAM。只有呼叫memset
使用記憶體後,虛擬記憶體才會真正對映到RAM。
9. 明明還有很多記憶體,但是發生OOM了。。
- 這種情況經常出現在生成Bitmap的時候。
- 在一個函式裡生成一個13m 的int陣列,再該函式結束後,按理說這個int陣列應該已經被釋放了,或者說可以釋放,這個13M的空間應該可以空出來。
- 這個時候如果你繼續生成一個10M的int陣列是沒有問題的,反而生成一個4M的Bitmap就會跳出OOM。這個就奇怪了,為什麼10M的int夠空間,反而4M的Bitmap不夠呢?
在Android中:
- 一個程序的記憶體可以由2個部分組成:java 使用記憶體 ,C 使用記憶體
這兩個記憶體的和必須小於16M,不然就會出現大家熟悉的OOM,這個就是第一種OOM的情況。 - 一旦記憶體分配給Java後,以後這塊記憶體即使釋放後,也只能給Java的使用
這個估計跟java虛擬機器裡把記憶體分成好幾塊進行快取的原因有關,反正C就別想用到這塊的記憶體了,所以如果Java突然佔用了一個大塊記憶體,即使很快釋放了:
C 能使用的記憶體 = 16M - Java某一瞬間佔用的最大記憶體
- 而 Bitmap的生成是通過malloc進行記憶體分配的,佔用的是C的記憶體,這個也就說明了,上述的4MBitmap無法生成的原因,因為在13M被Java用過後,剩下C能用的只有3M了。
二、瞭解dalvik的Garbage Collection
如圖所示:
- GC會選擇一些它瞭解 還存活的物件 作為 記憶體遍歷的根節點(GC Roots),比方說
thread stack中的變數
,JNI中的全域性變數
,zygote中的物件(class loader載入)
等,然後開始對heap進行遍歷。到最後,部分沒有直接或者間接引用到GC Roots的就是需要回收的垃圾,會被GC回收掉。
如下圖藍色部分。
三、常見的記憶體洩漏
1. 非靜態內部類 的靜態例項 容易造成記憶體洩漏
public class MainActivity extends Activity
{
// 非靜態內部類的靜態例項
static Demo sInstance = null;
@Override
public void onCreate(BundlesavedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (sInstance == null) {
sInstance= new Demo();
}
}
class Demo
{
void doSomething()
{
System.out.print("dosth.");
}
}
}
- 上面的程式碼中的
sInstance 例項
型別為靜態例項,在第一個MainActivity act1例項建立時,sInstance會獲得並一直持有act1的引用。 - 當MainAcitivity銷燬後重建,因為sInstance持有act1的引用,所以act1是無法被GC回收的,程序中會存在2個MainActivity例項(act1和重建後的MainActivity例項),這個act1物件就是一個無用的但一直佔用記憶體的物件,即無法回收的垃圾物件。
- 所以,對於lauchMode不是singleInstance的Activity, 應該避免在activity裡面例項化其非靜態內部類的靜態例項。
2. 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 2.3中要避免記憶體洩漏也是可以做到的,
// 在activity的onDestroy時呼叫
// sBackgroundDrawable.setCallback(null)。
- 再看看android 4.0的Drawable.Java對setCallback的實現:
public final void setCallback(Callback cb){
mCallback = newWeakReference<Callback> (cb);
}
以上2個例子的記憶體洩漏都是因為 Activity的 引用的生命週期 超越了Activity 物件的生命週期。也就是常說的 Context洩漏,因為activity就是context。
3. 避免context相關的記憶體洩漏,需要注意以下幾點
不要對activity的context長期引用
( 一個activity的引用的生存週期應該和activity的生命週期相同 )如果可以的話,儘量使用關於application的context來替代和activity相關的context
如果一個acitivity的非靜態內部類的生命週期不受控制,那麼避免使用它;正確的方法是 使用一個靜態的內部類,並且對它的外部類有一WeakReference,就像在ViewRootImpl中內部類W所做的那樣。
4. 使用handler時的記憶體問題
1) 我們知道,Handler通過傳送Message與主執行緒互動。
- Message發出之後是儲存在MessageQueue中的,有些Message也不是馬上就被處理的。
- 在Message中存在一個 target,是Handler的一個引用,如果Message在Queue中存在的時間越長,就會導致Handler無法被回收。
- 如果Handler是非靜態的,則會導致Activity或者Service不會被回收。 所以正確處理Handler等之類的內部類,應該將自己的Handler定義為靜態內部類。
2) HandlerThread的使用也需要注意:
- 當我們在activity裡面建立了一個HandlerThread,程式碼如下:
public classMainActivity extends Activity
{
@Override
public void onCreate(BundlesavedInstanceState)
{
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();
// mThread.getLooper().quit();
}
}
這個程式碼存在洩漏問題,因為 HandlerThread的run方法是一個死迴圈,它不會自己結束,執行緒的生命週期超過了activity生命週期,當橫豎屏切換,HandlerThread執行緒的數量會隨著activity重建次數的增加而增加。
應該在onDestroy時將執行緒停止掉:
mThread.getLooper().quit();
另外,對於不是HandlerThread的執行緒,也應該確保activity消耗後,執行緒已經終止,可以這樣做:在onDestroy時呼叫 mThread.join();
join( ) 的作用是:“等待該執行緒終止”,這裡需要理解的就是該執行緒是指的主執行緒等待子執行緒的終止。也就是:在子執行緒呼叫了join()方法後面的程式碼,只有等到子執行緒結束了才能執行。
5. 註冊某個物件後未反註冊
比如 註冊廣播接收器、註冊觀察者 等等。
假設我們希望在鎖屏介面(LockScreen)中,監聽系統中的電話服務以獲取一些資訊(如訊號強度等),則可以在
LockScreen
中定義一個PhoneStateListener的物件
,同時將它 註冊 到TelephonyManager服務
中。對於LockScreen物件,當需要顯示鎖屏介面的時候就會建立一個LockScreen物件,而當鎖屏介面消失的時候LockScreen物件就會被釋放掉。但是如果 在釋放LockScreen物件的時候忘記取消我們之前註冊的PhoneStateListener物件,則會導致LockScreen無法被GC回收。如果不斷的使鎖屏介面顯示和消失,則最終會由於大量的LockScreen物件沒有辦法被回收而引起OutOfMemory,使得system_process程序掛掉。
雖然有些系統程式,它本身好像是可以自動取消註冊的(當然不及時),但是我們還是 應該在我們的程式中明確的取消註冊,程式結束時應該把所有的註冊都取消掉。
6. 集合中物件沒清理造成的記憶體洩露
我們通常把一些物件的引用加入到了集合中,當我們不需要該物件時,如果沒有把它的引用從集合中清理掉,這樣這個集合就會越來越大。如果這個集合是static的話,那情況就更嚴重了。
- 比如某公司的ROM的鎖屏曾經就存在記憶體洩漏問題:
- 這個洩漏是因為LockScreen**每次顯示時會註冊幾個callback**,它們儲存在
KeyguardUpdateMonitor的ArrayList<InfoCallback>
、
ArrayList<SimStateCallback>
等ArrayList例項中。但是在LockScreen**解鎖後,這些callback沒有被remove掉**,導致ArrayList不斷增大, callback物件不斷增多。這些callback物件的size並不大,heap增長比較緩慢,需要長時間地使用手機才能出現OOM,由於鎖屏是駐留在system_server程序裡,所以導致結果是手機重啟。
7. 資源物件沒關閉造成的記憶體洩露
- 資源性物件 比如(
Cursor
,File檔案
等) 往往都用了一些緩衝,我們在不使用的時候,應該及時關閉它們,以便它們的緩衝及時回收記憶體。它們的緩衝不僅存在於Java虛擬機器內,還存在於Java虛擬機器外。 - 如果我們僅僅是把它的引用設定為null,而不關閉它們,往往會造成記憶體洩露。因為有些資源性物件,比如SQLiteCursor(在解構函式finalize(),如果我們沒有關閉它,它自己會調close()關閉),如果我們沒有關閉它,系統在回收它時也會關閉它,但是這樣的效率太低了。因此對於資源性物件在不使用的時候,應該立即呼叫它的close()函式,將其關閉掉,然後再置為null.
- 在我們的程式退出時一定要確保我們的資源性物件已經關閉。
8. 一些不良程式碼成記憶體壓力
有些程式碼並不造成記憶體洩露,但是它們或是 對不使用的記憶體沒進行有效及時的釋放,或是沒有有效的利用已有的物件而是頻繁的申請新記憶體,對記憶體的回收和分配造成很大影響的。
1) Bitmap使用不當
- 及時的銷燬
在用完Bitmap時,要及時的bitmap.recycle( )掉。
注意,recycle( )並不能確定立即就會將Bitmap釋放掉,但是會給虛擬機器一個暗示:“該圖片可以釋放了”。 - 設定取樣率
有時候,我們要顯示的區域很小,沒有必要將整個圖片都加載出來,而只需要記載一個縮小過的圖片,這時候可以設定一定的取樣率,那麼就可以大大減小佔用的記憶體。如下面的程式碼:
private ImageView preview;
BitmapFactory.Options options = newBitmapFactory.Options();
// 圖片寬高都為原來的二分之一,即圖片為原來的四分之一
options.inSampleSize = 2;
Bitmap bitmap = BitmapFactory.decodeStream(cr.openInputStream(uri),
null, options); preview.setImageBitmap(bitmap);
- 巧妙的運用軟引用(SoftRefrence)
有些時候,我們使用Bitmap後沒有保留對它的引用,因此就無法呼叫Recycle函式。這時候巧妙的運用軟引用,可以使Bitmap在記憶體快不足時得到有效的釋放。如下:
SoftReference<Bitmap> bitmap_ref = new SoftReference<Bitmap>(
BitmapFactory.decodeStream(inputstream));
...
...
if (bitmap_ref .get() != null) {
bitmap_ref.get().recycle();
}
2) 構造Adapter時,沒有使用快取的 convertView
初始時ListView會從BaseAdapter中根據當前的屏幕布局例項化一定數量的view物件,同時ListView會將這些view物件快取起來。
當向上滾動ListView時,原先位於最上面的list item的view物件會被回收,然後被用來構造新出現的最下面的list item。這個構造過程就是由
getView()
方法完成的,getView()
的第二個形參View convertView
就是被快取起來的list item的view物件 ( 初始化時快取中沒有 view 物件,則 convertView 是 null )。
由此可以看出,如果我們不去使用convertView,而是每次都在getView()中重新例項化一個View物件的話,即浪費時間,也造成記憶體垃圾,給垃圾回收增加壓力,如果垃圾回收來不及的話,虛擬機器將不得不給該應用程序分配更多的記憶體,造成不必要的記憶體開支。
3) 不要在經常呼叫的方法中建立物件,尤其是忌諱在迴圈中建立物件。
可以適當的使用 hashtable
, vector
建立一組物件容器,然後從容器中去取那些物件,而不用每次 new 之後又丟棄。
9. 查詢資料庫而沒有關閉Cursor
在Android中,Cursor是很常用的一個物件,但在寫程式碼時,經常會有人忘記呼叫close, 或者因為程式碼邏輯問題狀況導致close未被呼叫。
- 通常,在Activity中,我們可以呼叫startManagingCursor或直接使用managedQuery讓Activity自動管理Cursor物件。
但需要注意的是,當Activity結束後,Cursor將不再可用! - 若操作Cursor的程式碼和UI不同步(如後臺執行緒),需要先判斷Activity是否已經結束,或者在呼叫OnDestroy前,先等待後臺執行緒結束。
- 除此之外,以下也是比較常見的Cursor不會被關閉的情況:
try {
Cursor c = queryCursor();
int a = c.getInt(1);
......
c.close();
} catch (Exception e) {
}
// 雖然表面看起來,Cursor.close()已經被呼叫
// 但若出現異常,將會跳過close(),從而導致記憶體洩露。
// 所以,我們的程式碼應該以如下的方式編寫:
Cursor c = queryCursor();
try {
int a = c.getInt(1);
......
} catch (Exception e) {
} finally {
c.close(); // 在finally中呼叫close(), 保證其一定會被呼叫
}
10. 呼叫registerReceiver後未呼叫unregisterReceiver()
在呼叫registerReceiver後,若未呼叫unregisterReceiver,其所佔的記憶體是相當大的。
而我們經常可以看到類似於如下的程式碼:
registerReceiver(new BroadcastReceiver() {
...
}, filter); ...
這是個很嚴重的錯誤,因為它會導致BroadcastReceiver不會被unregister而導致記憶體洩露。
11. WebView物件沒有銷燬
當我們不要使用WebView物件時,應該呼叫它的destory()
函式來銷燬它,並釋放其佔用的記憶體,否則其佔用的記憶體長期也不能被回收,從而造成記憶體洩露。
12. GridView的濫用
GridView和ListView的實現方式不太一樣。GridView的View不是即時建立的,而是全部儲存在記憶體中的。比如一個GridView有100項,雖然我們只能看到10項,但是其實整個100項都是在記憶體中的。