本文主要是整理!
記憶體洩漏(memory leak)
什麼是記憶體洩漏
記憶體洩漏是指你申請了一塊記憶體,但沒有及時釋放,而這塊記憶體會一直占用無法在進行配置設定,
這樣就會出現記憶體洩漏。
java的GC記憶體回收機制
某對象不再有任何的引用的時候才會進行回收。
記憶體配置設定的政策
- 靜态的
靜态的存儲區:記憶體在程式編譯的時候就已經配置設定好,這塊的記憶體在程式整個運作期間都一直存在。
它主要存放靜态資料、全局的static資料和一些常量。
- 棧式的
在執行函數(方法)時,函數一些内部變量的存儲都可以放在棧上面建立,
函數執行結束的時候這些存儲單元就會自動被釋放掉。
注:
棧記憶體包括配置設定的運算速度很快,因為内置在處理器的裡面的。當然容量有限。
- 堆式的
也可以成為動态記憶體配置設定。常用的方式就是new來申請一塊記憶體。
java裡面是直接依賴的java的gc回收機制。
- 棧式的 和 堆式的的差別
1.堆是不連續的記憶體區域,堆空間比較靈活也特别大。
2.棧是一塊連續的記憶體區域,大小是有作業系統覺決定的。
3.堆管理很麻煩,頻繁地new/remove會造成大量的記憶體碎片,這樣就會慢慢導緻效率低下。
4.對于棧的話,它先進後出,進出完全不會産生碎片,運作效率高且穩定。
看下面代碼示例:
public class OOMMain {
// 堆
int a=0;
Book book1=new Book();
private void main(){
//棧
int b=9;
Book2 book2=new Book2();
}
}
A.成員變量全部存儲在堆中(包括基本資料類型,引用及引用的對象實體)—因為他們屬于類,類對象,最終還是要被new出來的。
B.局部變量的基本資料類型和引用存儲于棧當中,引用的對象實體存儲在堆中。-----因為他們屬于方法當中的變量,生命周期會随着方法一起結束。
從以上内容我們可以知道:記憶體洩露,主要讨論堆記憶體,它存放的就是引用指向的對象實體。
記憶體洩漏4種狀态
-
常發性記憶體洩漏。
發生記憶體洩漏的代碼會被多次執行到,每次被執行的時候都會導緻一塊記憶體洩漏。
-
偶發性記憶體洩漏。
發生記憶體洩漏的代碼隻有在某些特定環境或操作過程下才會發生。常發性和偶發性是相對的。對于特定的環境,偶發性的也許就變成了常發性的。是以測試環境和測試方法對檢測記憶體洩漏至關重要。
-
一次性記憶體洩漏。
發生記憶體洩漏的代碼隻會被執行一次,或者由于算法上的缺陷,導緻總會有一塊僅且一塊記憶體發生洩漏。比如,在類的構造函數中配置設定記憶體,在析構函數中卻沒有釋放該記憶體,是以記憶體洩漏隻會發生一次。
-
隐式記憶體洩漏。
程式在運作過程中不停的配置設定記憶體,但是直到結束的時候才釋放記憶體。嚴格的說這裡并沒有發生記憶體洩漏,因為最終程式釋放了所有申請的記憶體。但是對于一個伺服器程式,需要運作幾天,幾周甚至幾個月,不及時釋放記憶體也可能導緻最終耗盡系統的所有記憶體。是以,我們稱這類記憶體洩漏為隐式記憶體洩漏。
危害
- 過多的記憶體洩露最終會導緻記憶體溢出(OOM)
- 記憶體洩露導緻可用記憶體不足,會觸發頻繁GC,不管是Android2.2以前的單線程GC還是現在的CMS和G1,都有一部分的操作會導緻使用者線程停止(就是所謂的Stop the world),進而導緻UI卡頓。
産生的常見原因
- 資源未關閉造成的記憶體洩漏
BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等資源的使用,應該在Activity銷毀時及時關閉或者登出,否則這些資源将不會被回收,造成記憶體洩漏。
- 記得登出監聽器
注冊監聽器的時候會add Listener,不要忘記在不需要的時候remove掉Listener
- 單例造成的記憶體洩漏
單例的靜态特性使得單例的生命周期和應用的生命周期一樣長,這就說明了如果一個對象已經不需要使用了,而單例對象還持有該對象的引用,那麼這個對象将不能被正常回收,這就導緻了記憶體洩漏。
看一個經典案例:
//單例需要傳入一個Context,是以這個Context的生命周期的長短至關重要:
public class AppManager {
private static AppManager instance;
private Context context;
private AppManager(Context context) {
this.context = context.getApplicationContext();
//1.這裡傳入一個Application的Context:這将沒有任何問題,因為單例的生命周期和Application的一樣長
//this.context = context;
//2、傳入的是Activity的Context:當這個Context所對應的Activity退出時,由于該Context和Activity的生命周期一樣長(Activity間接繼承于Context),是以目前Activity退出時它的記憶體并不會被回收,因為單例對象持有該Activity的引用。
}
public static AppManager getInstance(Context context) {
if (instance != null) {
instance = new AppManager(context);
}
return instance;
}
}
- 線程造成的記憶體洩漏
//-------------------第一種
new AsyncTask() {
@Override
protected Void doInBackground(Void... params) {
SystemClock.sleep(100000);
return null;
}
}.execute();
//-------------------第二種
new Thread(new Runnable() {
@Override
public void run() {
SystemClock.sleep(100000);
}
}).start();
這裡的異步任務和Runnable都是一個匿名内部類,是以它們對目前Activity都有一個隐式引用。如果Activity在銷毀之前,任務還未完成,那麼将導緻Activity的記憶體資源無法回收,造成記憶體洩漏 。
正确的寫法:使用靜态内部類的寫法
static class MyAsyncTask extends AsyncTask {
private WeakReference weakReference;
public MyAsyncTask(Context context) {
weakReference = new WeakReference<>(context);
}
@Override
protected Void doInBackground(Void... params) {
SystemClock.sleep(10000);
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);
MainActivity activity = (MainActivity) weakReference.get();
if (activity != null) {
//...
}
}
}
static class MyRunnable implements Runnable{
@Override
public void run() {
SystemClock.sleep(10000);
}
}
//——————
new Thread(new MyRunnable()).start();
new MyAsyncTask(this).execute();
這樣就避免了Activity的記憶體資源洩漏,當然在Activity銷毀時候也應該取消相應的任務AsyncTask::cancel(),避免任務在背景執行浪費資源。
-
Handler造成的記憶體洩漏
Handler的使用代碼編寫不規範即有可能造成記憶體洩漏,如下示例:
public class MainActivity extends AppCompatActivity {
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
//...
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
loadData();
}
private void loadData(){
//...request
Message message = Message.obtain();
mHandler.sendMessage(message);
}
}
由于mHandler是Handler的非靜态匿名内部類的執行個體,是以它持有外部類Activity的引用,我們知道消息隊列是在一個Looper線程中不斷輪詢處理消息,那麼當這個Activity退出時消息隊列中還有未處理的消息或者正在處理消息,而消息隊列中的Message持有mHandler執行個體的引用,mHandler又持有Activity的引用,是以導緻該Activity的記憶體資源無法及時回收,引發記憶體洩漏
正确寫法:
public class MainActivity extends AppCompatActivity {
private MyHandler mHandler = new MyHandler(this);
private TextView mTextView ;
private static class MyHandler extends Handler {
private WeakReference reference;
public MyHandler(Context context) {
reference = new WeakReference<>(context);
}
@Override
public void handleMessage(Message msg) {
MainActivity activity = (MainActivity) reference.get();
if(activity != null){
activity.mTextView.setText("");
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextView = (TextView)findViewById(R.id.textview);
loadData();
}
private void loadData() {
//...request
Message message = Message.obtain();
mHandler.sendMessage(message);
}
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
}
}
使用mHandler.removeCallbacksAndMessages(null);是移除消息隊列中所有消息和所有的Runnable。當然也可以使用mHandler.removeCallbacks();或mHandler.removeMessages();來移除指定的Runnable和Message。
如何避免記憶體洩漏
-
使用輕量的資料結構
使用ArrayMap/SparseArray來代替HashMap,ArrayMap/SparseArray是專門為移動裝置設計的高效的資料結構
1.SparseArray
a.支援int類型,避免自動裝箱,但是也隻支援int類型的key
b.内部通過兩個數組來進行資料存儲的,一個存儲key,另外一個存儲value
c.因為key是int,在查找時,采用二分查找,效率高,SparseArray存儲的元素都是按元素的key值從小到大排列好的。 (Hashmap通過周遊Entry數組來擷取對象)
d.預設初始size為0,每次增加元素,size++
- ArrayMap
a.跟SparseArray一樣,内部兩個數組,但是第一個存key的hash值,一個存value,對象按照key的hash值排序,二分查找也是按照hash b.查找index時,傳入key,計算出hash,通過二分查找hash數組,确定index
- 不要使用Enum
- Bitmap的處理
1.Bitmap壓縮(品質壓縮/尺寸壓縮) 2. Lru機制處理Bitmap,也可以使用那些有名的圖檔緩存架構。
關于這個Lru大緻提一下:Picasso和glide(大部分都是基于LruCache的)。
LruCache近期最少使用算法的緩存機制,LruCache内部擁有一個LinkedHashMap,其key 為索引,value為Bitmap。LinkedHashMap的内部實作儲存了添加的順序,是以,當LruCache裡面的緩存比設定的 最大值大時,就會把最先添加的那些緩存清除掉。
linkedHashMap:
記憶體溢出(oom)
定義
我們需要一定記憶體的大小,但是系統無法配置設定給我們,滿足不了我們的需求,是以導緻oom
産生原因及如何避免
- 圖檔過大導緻 OOM
可以對圖檔進行:品質壓縮或尺寸壓縮
- Bitmap 對象不再使用時調用 recycle()釋放記憶體
- 查詢資料庫沒有關閉遊标
-
界面切換導緻 OOM
有時候我們會發現這樣的問題,橫豎屏切換 N 次後 OOM 了。
這種問題沒有固定的解決方法,但是我們可以從以下幾個方面下手分析。
1.看看頁面布局當中有沒有大的圖檔,比如背景圖之類的。
/**去除 xml 中相關設定,改在程式中設定背景圖(放在 onCreate()方法中):
*/
Drawable drawable = getResources().getDrawable(R.drawable.id);
ImageView imageView = new ImageView(this);
imageView.setBackgroundDrawable(drawable);
在 Activity destory 時注意,drawable.setCallback(null);防止 Activity 得不到及時的釋放。
- 記憶體洩漏造成的記憶體溢出
- 其他
Android 應用程式中最典型的需要注意釋放資源的情況是在 Activity 的生命周期中,在 onPause()、onStop()、onDestroy()方法中需要适當的釋放資源的情況。