一、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項都是在內存中的。