內存泄漏分析,工具,泄漏的情況

一:內存泄漏的概念

內存泄漏:內存不再GC的掌控中。也就是一個對象不再需要使用,本該被回收時,但是有另外一個對象持有該對象,導致對象不能回收。這種導致本該回收的對象不能回收且停留在堆內存中,這就是內存泄漏。

內存分配策略:

  • 靜態內存:存放靜態數據,這塊內存是在編譯時就已經分配好的,在整個運行期間都存在。他主要存放靜態數據、全局的static數據和一些常量。
  • 棧內存:程序執行是,局部變量的創建存儲區,執行結束後會自動釋放。
  • 堆內存:也叫動態內存分配。有時可以用malloc或者new來申請分配一個內存。在C/C++可能需要自己負責釋放(java裏面直接依賴GC機制)。在C/C++這裏是可以自己掌控內存的,需要有很高的素養來解決內存的問題。java在這一塊貌似程序員沒有很好的方法自己去解決垃圾內存,需要的是編程的時候就要注意自己良好的編程習慣。

區別:1:堆是不連續的內存區域,堆空間比較靈活也特別大。
           2: 棧式一塊連續的內存區域,大小是有操作系統覺決定的。

           3:堆管理很麻煩,頻繁地new/remove會造成大量的內存碎片,這樣就會慢慢導致效率低下。對於棧的話,他先進後出,進出完全不會產生碎片,運行效率高且穩定。

public class Student{
    int a = 1;
    Dimen  d1= new Dimen ();
    public void XXX(){
        int b = 1;//棧裏面
        Dimen d2 = new Dimen ();
    }

}

1.成員變量全部存儲在堆中(包括基本數據類型,引用及引用的對象實體)---因爲他們屬於類,類對象最終還是要被new出來的。
2.局部變量的基本數據類型和引用存儲於棧當中,引用的對象實體存儲在堆中。-----因爲他們屬於方法當中的變量,生命週期會隨着方法一起結束。

 

內存泄露,主要討論堆內存,他存放的就是引用指向的對象實體。

內存泄露(Memory Leak):
    進程中某些對象已經沒有使用價值了,但是他們卻還可以直接或者間接地被引用到GC Root導致無法回收。
    當內存泄露過多的時候,再加上應用本身佔用的內存,日積月累最終就會導致內存溢出OOM.
內存溢出(OOM):    
    當應用佔用的heap資源超過了Dalvik虛擬機分配的內存就會內存溢出。比如:加載大圖片。

二:內存泄漏的情況

OutOfMenoryError主要有以下幾種情況:

1:在ListView或者GridView 等加載大量數據或圖片,構造adapter沒有使用緩存ContentView

@Override  
public View getView(int position, View convertView, ViewGroup parent) {  
    ViewHolder vHolder = null;  
    //如果convertView對象爲空則創建新對象,不爲空則複用  
    if (convertView == null) {  
        convertView = inflater.inflate(..., null);  
        // 創建 ViewHodler 對象  
        vHolder = new ViewHolder();  
        vHolder.img= (ImageView) convertView.findViewById(...);  
        vHolder.tv= (TextView) convertView  
                .findViewById(...);  
        // 將ViewHodler保存到Tag中  
        convertView.setTag(vHolder);  
    } else {  
        //當convertView不爲空時,通過getTag()得到View  
        vHolder = (ViewHolder) convertView.getTag();  
    }  
    // 給對象賦值,修改顯示的值  
    vHolder.img.setImageBitmap(...);  
    vHolder.tv.setText(...);  
    return convertView;  
}  

static class ViewHolder {  
    TextView tv;  
    ImageView img;  
} 

   圖片非常佔用內存,一定要管理好內存,不然很容易內存溢出。
    滑出去的圖片就回收,節省內存。看ListView的源碼----回收對象,還會重用ConvertView。
    如果用戶反覆滑動或者下面還有同樣的圖片,就會造成多次重複IO(很耗時),
    那麼需要緩存---平衡好內存大小和IO,算法和一些特殊的java類。
    算法:lrucache(最近最少使用先回收)
    特殊的java類:利於回收,StrongReference,SoftReference,WeakReference,PhatomReference
        
StrongReference強引用:
    回收時機:從不回收 使用:對象的一般保存  生命週期:JVM停止的時候纔會終止
SoftReference,軟引用
    回收時機:當內存不足的時候;使用:SoftReference<String>結合ReferenceQueue構造有效期短;生命週期:內存不足時終止
WeakReference,弱引用
    回收時機:在垃圾回收的時候;使用:同軟引用; 生命週期:GC後終止
PhatomReference 虛引用
    回收時機:在垃圾回收的時候;使用:合ReferenceQueue來跟蹤對象唄垃圾回收期回收的活動; 生命週期:GC後終止

開發時,爲了防止內存溢出,處理一些比較佔用內存大並且生命週期長的對象的時候,可以儘量使用軟引用和弱引用。
軟引用比LRU算法更加任性,回收量是比較大的,你無法控制回收哪些對象。

比如使用場景:默認頭像、默認圖標。
ListView或者GridView等要使用內存緩存+外部緩存(SD卡)

2:資源爲關閉引起的內存泄漏

BroadCastReceiver:registerReceiver()unregisterReceiver()要成對出現,通常需要在ActivityonDestory()方法去取消註冊廣播接收者。

Cursor:操作完close。

Bitmap:使用後未調用recycle()。

IO流:用完後及時關閉。

自定義屬性attribute:attr.recycle()回收。


當不需要使用的時候,要記得及時釋放資源。否則就會內存泄露。

3:不需要用的監聽未移除會發生內存泄露

  例子1:
//        tv.setOnClickListener();//監聽執行完回收對象
        //add監聽,放到集合裏面
        tv.getViewTreeObserver().addOnWindowFocusChangeListener(new ViewTreeObserver.OnWindowFocusChangeListener() {
            @Override
            public void onWindowFocusChanged(boolean b) {
                //監聽view的加載,view加載出來的時候,計算他的寬高等。

                //計算完後,一定要移除這個監聽
                tv.getViewTreeObserver().removeOnWindowFocusChangeListener(this);
            }
        });

    例子2:

       SensorManager sensorManager = getSystemService(SENSOR_SERVICE);
        Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
        sensorManager.registerListener(this,sensor,SensorManager.SENSOR_DELAY_FASTEST);
        //不需要用的時候記得移除監聽
        sensorManager.unregisterListener(listener);

4:集合類泄漏

集合類中如果只有添加元素的方法,而沒有相應的刪除機制,導致內存被佔用。如果這個集合類是全局性的變量(比如類中的靜態屬性),那麼沒有 響應的刪除機制,很可能導致集合所佔用的內存只增不減。

5:無限循環動畫
沒有在onDestroy中停止動畫,否則Activity就會變成泄露對象。
比如:輪播圖效果。

6:Context泄漏 這是一個很隱晦的OutOfMemoryError的情況。先看一個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);  
}  

這段代碼效率很快,但同時又是極其錯誤的:
我們看一下setBackgroundDrawable(Drawable background)的源碼:

 public void setBackgroundDrawable(Drawable background) {
	...
	background.setCallback(this);
 }

background.setCallback(this);方法,也就是說Drawable擁有TextView的引用,而TextView又擁有Activity*(Context類型)*的引用, 因爲sBackgroundstatic的,即使Activity被銷燬,但是sBackground的生命週期還沒走完,所以內存仍然不會被釋放。這樣就會有內存泄露了。 對,這樣想是對的,但是我們看一下setCallback的源碼:

  public final void setCallback(Callback cb) {
    mCallback = new WeakReference<Callback>(cb);
}

我們會發現裏面使用了WeakReference,所以不會存在內存泄露了,但是官網當時提供的例子明明說有泄露,這是因爲在3.0之後, 修復了這個內存泄露的問題,在3.0之前setCallback,方法是沒有使用WeakReference的,所以這種泄露的情況在3.0之前會發生,3.0之後已經被修復。

7: 線程也是造成內存泄露的一個重要的源頭。線程產生內存泄露的主要原因在於線程生命週期的不可控。我們來考慮下面一段代碼。

public class MyActivity extends Activity {     
	@Override     
	public void onCreate(Bundle savedInstanceState) {         
		super.onCreate(savedInstanceState);         
		setContentView(R.layout.main);         
		new MyThread().start();     
	}       
	private class MyThread extends Thread{         
	@Override         
		public void run() {             
		super.run();             
		//耗時的操作       
		}     
	} 
}  

假設MyThreadrun函數是一個很費時的操作,當調用finish的時候Activity會銷燬掉嗎?
事實上由於我們的線程是Activity的內部類,所以MyThread中保存了Activity的一個引用,當MyThreadrun函數沒有結束時,MyThread是不會被銷燬的, 因此它所引用的Activity也不會被銷燬,因此就出現了內存泄露的問題。

8:單例造成的內存泄漏
由於單例的靜態特性使得其生命週期跟應用的生命週期一樣長,所以如果使用不恰當的話,很容易造成內存泄漏,比如:

public class AppManager {
	private static AppManager instance;
	private Context context;
	private AppManager(Context context) {
		this.context = context;
	}
	public static AppManager getInstance(Context context) {
		if (instance != null) {
			instance = new AppManager(context);
		}
		return instance;
	}
}

這裏如果傳入的是ActivityContext,當該ContextActivity退出後,由於其被單例對象引用,所以會導致Activity無法被回收,就造成內存泄漏。

:9:儘量使用ApplicationContext Context引用的生命週期超過它本身的生命週期,也會導致Context泄漏。 所以如果打算保存一個長時間的對象時儘量使用Application這種Context類型。 例如:

mStorageManager = (StorageManager) getSystemService(Context.STORAGE_SERVICE);
改成:
mStorageManager = (StorageManager) getApplicationContext().getSystemService(Context.STORAGE_SERVICE);

按道理來說這種系統服務是不會有問題,但是有些廠商在修改的時候,可能會導致Context無法被及時釋放。

10:Handler的使用,在Activity退出的時候注意移除(尤其是循環的時候)

public class ThreadDemo extends Activity {  
	private static final String TAG = "ThreadDemo";  
	private int count = 0;  
	private Handler mHandler =  new Handler();  
	  
	private Runnable mRunnable = new Runnable() {  
		  
		public void run() {  
			//爲了方便 查看,我們用Log打印出來   
			Log.e(TAG, Thread.currentThread().getName() + " " +count);    
			//每2秒執行一次   
			mHandler.postDelayed(mRunnable, 2000);  
		}   
	};  
	@Override  
	public void onCreate(Bundle savedInstanceState) {  
		super.onCreate(savedInstanceState);  
		setContentView(R.layout.main);   
		//通過Handler啓動線程   
		mHandler.post(mRunnable);  
	} 
} 

這樣在也會引發內存泄露。我們應該在onDestory方法中移除Handler,代碼如下:

@Override  
protected void onDestroy() {  
	super.onDestroy();  
	mHandler.removeCallbacks(mRunnable);  
} 

 

11:由上面的Handler可以引伸出來的匿名內部類、非靜態內部類和異步線程導致的內存泄漏。
下面看一個非靜態內部類創建靜態實例導致的內存泄漏

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的引用而造成內存泄漏。
下面再看一個匿名內部類和異步現成的現象:

public class MainActivity extends Activity {
	...
	Runnable ref1 = new MyRunable();
	Runnable ref2 = new Runnable() {
		@Override
		public void run() {

		}
	};
	...
}

上面的例子中ref1對象是沒問題的,但是ref2這個匿名類的實現對象中有外部類的引用,如果此時線程的生命週期與Activity的不一致時就會造成了泄漏。

總結一下避免Context泄漏應該注意的問題:

  • 使用getApplicationContext()類型。
  • 注意對Context的引用不要超過它本身的生命週期。
  • 慎重的使用static關鍵字。
  • Activity裏如果有線程或Handler時,一定要在onDestroy()裏及時停掉。

三:內存泄漏的判斷的工具

性能優化的幫助工具:
    MAT,
    Memory Monitor(屬於AndroidMonitor中一個模塊),
    HeapTool(查看堆信息),
    Allaction Tracking,
    LeakCanary
    Lint工具

1:Allaction Tracking
追蹤內存分配信息。可以很直觀地看到某個操作的內存是如何進行一步一步地分配的。


2:LeakCanary
Square公司
可以直接在手機端查看內存泄露的工具
實現原理:本質上還是用命令控制生成hprof文件分析檢查內存泄露。
然後發送通知。
Application
    install()
LeakCanary
    androidWatcher()
RefWatcher
    new AndroidWatcherExecutor() --->dumpHeap()/analyze()(--->runAnalysis())--->Hprof文件分析
    new AndroidHeapDumper()
    new ServiceHeapDumpListener


3:Lint分析工具
Android Studio很方便 很好用。
    檢測資源文件是否有沒有用到的資源。
    檢測常見內存泄露
    安全問題SDK版本安全問題
    是否有費的代碼沒有用到
    代碼的規範---甚至駝峯命名法也會檢測
    自動生成的羅列出來
    沒用的導包
    可能的bug

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章