參考
內(nèi)存泄露從入門到精通三部曲之基礎(chǔ)知識(shí)篇
Android 內(nèi)存泄漏總結(jié)
Android內(nèi)存泄漏研究
Android內(nèi)存優(yōu)化之——static使用篇
避免Android中Context引起的內(nèi)存泄露
Android 內(nèi)存泄漏案例和解析 附RXJAVA內(nèi)存泄露
一、方法區(qū) 棧區(qū) 堆區(qū)
參考
為什么要有堆區(qū)和棧區(qū)呢
Java里的堆(heap)棧(stack)和方法區(qū)(method)
結(jié)構(gòu)化語言里函數(shù)(子程序)調(diào)用最方便的實(shí)現(xiàn)方式就是用棧,以至于現(xiàn)在絕大部分芯片都對(duì)棧提供芯片級(jí)的硬件支持,一條指令即可搞定棧的pop操作。棧的好處是:方便、快、有效避免內(nèi)存碎片化。棧的問題是:不利于管理大內(nèi)存(尤其在16位和32位時(shí)代)、數(shù)據(jù)的生命周期難于控制(棧內(nèi)的有效數(shù)據(jù)通常是連續(xù)存儲(chǔ)的,所以pop時(shí)后申請(qǐng)的內(nèi)存必須早于先申請(qǐng)的內(nèi)存失效),所以棧不利于動(dòng)態(tài)地管理并且有效地利用寶貴的內(nèi)存資源。于是我們有了堆。。。
按照編譯原理的觀點(diǎn),程序運(yùn)行時(shí)的內(nèi)存分配有三種策略,分別是靜態(tài)的,棧式的,和堆式的,對(duì)應(yīng)的,三種存儲(chǔ)策略使用的內(nèi)存空間主要分別是靜態(tài)存儲(chǔ)區(qū)(也稱方法區(qū))、堆區(qū)和棧區(qū)。他們的功能不同,對(duì)他們使用方式也就不同。
靜態(tài)存儲(chǔ)區(qū)(方法區(qū)):
內(nèi)存在程序編譯的時(shí)候就已經(jīng)分配好,這塊內(nèi)存在程序整個(gè)運(yùn)行期間都存在。它主要存放靜態(tài)數(shù)據(jù)、全局static數(shù)據(jù)和常量。
棧區(qū):
棧內(nèi)存分配運(yùn)算內(nèi)置于處理器的指令集中,效率很高,但是分配的內(nèi)存容量有限。在函數(shù)中(說明是局部變量)定義的一些基本類型的變量和對(duì)象的引用變量都是在函數(shù)的棧內(nèi)存中分配。當(dāng)在一段代碼塊中定義一個(gè)變量時(shí),java就在棧中為這個(gè)變量分配內(nèi)存空間,當(dāng)超過變量的作用域后,java會(huì)自動(dòng)釋放掉為該變量分配的內(nèi)存空間,該內(nèi)存空間可以立刻被另作他用。
堆區(qū):
亦稱動(dòng)態(tài)內(nèi)存分配。程序在運(yùn)行的時(shí)候用malloc或new申請(qǐng)任意大小的內(nèi)存,程序員自己負(fù)責(zé)在適當(dāng)?shù)臅r(shí)候用free或delete釋放內(nèi)存(Java則依賴?yán)厥掌鳎6褍?nèi)存用于存放所有由new創(chuàng)建的對(duì)象(內(nèi)容包括該對(duì)象其中的所有成員變量)和數(shù)組。動(dòng)態(tài)內(nèi)存的生存期可以由我們決定,如果我們不釋放內(nèi)存,程序?qū)⒃谧詈蟛裴尫诺魟?dòng)態(tài)內(nèi)存。 但是,良好的編程習(xí)慣是:如果某動(dòng)態(tài)內(nèi)存不再使用,需要將其釋放掉。
堆是不連續(xù)的內(nèi)存區(qū)域(因?yàn)橄到y(tǒng)是用鏈表來存儲(chǔ)空閑內(nèi)存地址,自然不是連續(xù)的),堆大小受限于計(jì)算機(jī)系統(tǒng)中有效的虛擬內(nèi)存(32bit系統(tǒng)理論上是4G),所以堆的空間比較靈活,比較大。棧是一塊連續(xù)的內(nèi)存區(qū)域,大小是操作系統(tǒng)預(yù)定好的,windows下棧大小是2M(也有是1M,在編譯時(shí)確定,VC中可設(shè)置)。
對(duì)于堆,頻繁的new/delete會(huì)造成大量內(nèi)存碎片,使程序效率降低。對(duì)于棧,它是先進(jìn)后出的隊(duì)列,進(jìn)出一一對(duì)應(yīng),不產(chǎn)生碎片,運(yùn)行效率穩(wěn)定高。
至此,我們來看看Java中需要被回收的垃圾:
{
Person p1 = new Person();
……
}
引用句柄p1的作用域是從定義到“}”處,執(zhí)行完這對(duì)大括號(hào)中的所有代碼后,產(chǎn)生的Person對(duì)象就會(huì)變成垃圾,因?yàn)橐眠@個(gè)對(duì)象的句柄p1已超過其作用域,p1失效,在棧中被銷毀,因此堆上的Person對(duì)象不再被任何句柄引用了。 因此person變?yōu)槔瑫?huì)被回收。
二、常見例子
1.以下Activity無法正常銷毀,因?yàn)殪o態(tài)變量sContext引用了它。靜態(tài)變量的生命周期,起始于類的加載,終止于類的釋放。
public class MainActivity extends Activity{
private static final String TAG = "MainActivity";
private static Context sContext;
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
sContext = this;
}
}
public class MainActivity extends Activity{
private static final String TAG = "MainActivity";
private static View sView;
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
sContext = new View(this);
}
}
解決辦法就是在這個(gè) Activity 的 onDestroy 時(shí)將 sContext 的值置空,或者避免使用靜態(tài)變量這樣的寫法。
2.非靜態(tài)內(nèi)部類
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內(nèi)部創(chuàng)建了一個(gè)非靜態(tài)內(nèi)部類的單例,每次啟動(dòng)Activity時(shí)都會(huì)使用該單例的數(shù)據(jù),這樣雖然避免了資源的重復(fù)創(chuàng)建,不過這種寫法卻會(huì)造成內(nèi)存泄漏,因?yàn)榉庆o態(tài)內(nèi)部類默認(rèn)會(huì)持有外部類的引用,而該非靜態(tài)內(nèi)部類又創(chuàng)建了一個(gè)靜態(tài)的實(shí)例,該實(shí)例的生命周期和應(yīng)用的一樣長,這就導(dǎo)致了該靜態(tài)實(shí)例一直會(huì)持有該Activity的引用,導(dǎo)致Activity的內(nèi)存資源不能正常回收。正確的做法為:將該內(nèi)部類設(shè)為靜態(tài)內(nèi)部類或?qū)⒃搩?nèi)部類抽取出來封裝成一個(gè)單例。
3.匿名內(nèi)部類
android開發(fā)經(jīng)常會(huì)繼承實(shí)現(xiàn)Activity/Fragment/View,此時(shí)如果你使用了匿名類,并被異步線程持有了,那要小心了,如果沒有任何措施這樣一定會(huì)導(dǎo)致泄露。
public class MainActivity extends Activity {
...
Runnable ref1 = new MyRunable();
Runnable ref2 = new Runnable() {
@Override
public void run() {
}
};
...
}
ref1和ref2的區(qū)別是,ref2使用了匿名內(nèi)部類。我們來看看運(yùn)行時(shí)這兩個(gè)引用的內(nèi)存:
可以看到,ref1沒什么特別的。但ref2這個(gè)匿名類的實(shí)現(xiàn)對(duì)象里面多了一個(gè)引用:this$0這個(gè)引用指向MainActivity.this,也就是說當(dāng)前的MainActivity實(shí)例會(huì)被ref2持有,如果將這個(gè)引用再傳入一個(gè)異步線程,此線程和此Acitivity生命周期不一致的時(shí)候,就造成了Activity的泄露。
4.Handler
Handler 的使用造成的內(nèi)存泄漏問題應(yīng)該說是最為常見了,很多時(shí)候我們?yōu)榱吮苊?ANR 而不在主線程進(jìn)行耗時(shí)操作,在處理網(wǎng)絡(luò)任務(wù)或者封裝一些請(qǐng)求回調(diào)等api都借助Handler來處理,但 Handler 不是萬能的,對(duì)于 Handler 的使用代碼編寫一不規(guī)范即有可能造成內(nèi)存泄漏。另外,我們知道 Handler、Message 和 MessageQueue 都是相互關(guān)聯(lián)在一起的,萬一 Handler 發(fā)送的 Message 尚未被處理,則該 Message 及發(fā)送它的 Handler 對(duì)象將被線程 MessageQueue 一直持有。
由于 Handler 屬于 TLS(Thread Local Storage) 變量, 生命周期和 Activity 是不一致的。因此這種實(shí)現(xiàn)方式一般很難保證跟 View 或者 Activity 的生命周期保持一致,故很容易導(dǎo)致無法正確釋放。
舉個(gè)例子:
public class SampleActivity extends Activity {
private final Handler mLeakyHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// ...
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Post a message and delay its execution for 10 minutes.
mLeakyHandler.postDelayed(new Runnable() {
@Override
public void run() { /* ... */ }
}, 1000 * 60 * 10);
// Go back to the previous Activity.
finish();
}
}
在該 SampleActivity 中聲明了一個(gè)延遲10分鐘執(zhí)行的消息 Message,mLeakyHandler 將其 push 進(jìn)了消息隊(duì)列 MessageQueue 里。當(dāng)該 Activity 被 finish() 掉時(shí),延遲執(zhí)行任務(wù)的 Message 還會(huì)繼續(xù)存在于主線程中,它持有該 Activity 的 Handler 引用,所以此時(shí) finish() 掉的 Activity 就不會(huì)被回收了從而造成內(nèi)存泄漏(因 Handler 為非靜態(tài)內(nèi)部類,它會(huì)持有外部類的引用,在這里就是指 SampleActivity)。
修復(fù)方法:在 Activity 中避免使用非靜態(tài)內(nèi)部類,比如上面我們將 Handler 聲明為靜態(tài)的,則其存活期跟 Activity 的生命周期就無關(guān)了。同時(shí)通過弱引用的方式引入 Activity,避免直接將 Activity 作為 context 傳進(jìn)去,見下面代碼:
public class SampleActivity extends Activity {
/**
* Instances of static inner classes do not hold an implicit
* reference to their outer class.
*/
private static class MyHandler extends Handler {
private final WeakReference<SampleActivity> mActivity;
public MyHandler(SampleActivity activity) {
mActivity = new WeakReference<SampleActivity>(activity);
}
@Override
public void handleMessage(Message msg) {
SampleActivity activity = mActivity.get();
if (activity != null) {
// ...
}
}
}
private final MyHandler mHandler = new MyHandler(this);
/**
* Instances of anonymous classes do not hold an implicit
* reference to their outer class when they are "static".
*/
private static final Runnable sRunnable = new Runnable() {
@Override
public void run() { /* ... */ }
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Post a message and delay its execution for 10 minutes.
mHandler.postDelayed(sRunnable, 1000 * 60 * 10);
// Go back to the previous Activity.
finish();
}
}
綜述,即推薦使用靜態(tài)內(nèi)部類 + WeakReference 這種方式。每次使用前注意判空。
5.單例模式
Activity對(duì)象被單例模式的TestManager所持有,而單例模式特點(diǎn)是其生命周期與Application保持一致。
例1
package com.ryg.chapter_15.manager;
import java.util.ArrayList;
import java.util.List;
public class TestManager {
private List<OnDataArrivedListener> mOnDataArrivedListeners = new ArrayList<OnDataArrivedListener>();
private static class SingletonHolder {
public static final TestManager INSTANCE = new TestManager();
}
private TestManager() {
}
public static TestManager getInstance() {
return SingletonHolder.INSTANCE;
}
public synchronized void registerListener(OnDataArrivedListener listener) {
if (!mOnDataArrivedListeners.contains(listener)) {
mOnDataArrivedListeners.add(listener);
}
}
public synchronized void unregisterListener(OnDataArrivedListener listener) {
mOnDataArrivedListeners.remove(listener);
}
public interface OnDataArrivedListener {
public void onDataArrived(Object data);
}
}
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TestManager.getInstance().registerListener(this);
}
在銷毀前記得注銷掉即可
@Override
protected void onDestroy() {
testManager.unregisterListener(mMyListener);
super.onDestroy();
}
例2
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;
}
}
如果此時(shí)傳入的是 Activity 的 Context,當(dāng)這個(gè) Context 所對(duì)應(yīng)的 Activity 退出時(shí),由于該 Context 的引用被單例對(duì)象所持有,其生命周期等于整個(gè)應(yīng)用程序的生命周期,所以當(dāng)前 Activity 退出時(shí)它的內(nèi)存并不會(huì)被回收,這就造成泄漏了。
正確的方式應(yīng)該改為下面這種方式:
this.context = context.getApplicationContext();// 使用Application 的context
6.集合類泄漏
集合類如果僅僅有添加元素的方法,而沒有相應(yīng)的刪除機(jī)制,導(dǎo)致內(nèi)存被占用。如果這個(gè)集合類是全局性的變量 (比如類中的靜態(tài)屬性,全局性的 map 等即有靜態(tài)引用或 final 一直指向它),那么沒有相應(yīng)的刪除機(jī)制,很可能導(dǎo)致集合所占用的內(nèi)存只增不減。比如上面的典型例子就是其中一種情況,當(dāng)然實(shí)際上我們?cè)陧?xiàng)目中肯定不會(huì)寫這么 2B 的代碼,但稍不注意還是很容易出現(xiàn)這種情況,比如我們都喜歡通過 HashMap 做一些緩存之類的事,這種情況就要多留一些心眼。
7.屬性動(dòng)畫導(dǎo)致內(nèi)存泄露
屬性動(dòng)畫如果無限循環(huán),需要在destory中將其停止。
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
sContext = this;
ObjectAnimator animator = ObjectAnimator.ofFloat(mButton, "rotation",0, 360).setDuration(2000);
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.start();
//animator.cancel();在destory中調(diào)用cancel即可
8.RxJava 使用不當(dāng)造成內(nèi)存泄漏
RxJava 是一個(gè)非常易用且優(yōu)雅的異步操作庫。對(duì)于異步的操作,如果沒有及時(shí)取消訂閱,就會(huì)造成內(nèi)存泄漏:
Observable.interval(1, TimeUnit.SECONDS)
.subscribe(new Action1<Long>() {
@Override public void call(Long aLong) {
// pass
}
});
同樣是匿名內(nèi)部類造成的引用沒法被釋放,使得如果在 Activity 中使用就會(huì)導(dǎo)致它無法被回收,即使我們的 Action1 看起來什么也沒有做。解決辦法就是接收 subscribe 返回的 Subscription 對(duì)象,在 Activity onDestroy 的時(shí)候?qū)⑵淙∠嗛喖纯桑?/p>
public class LeakActivity extends AppCompatActivity {
private Subscription mSubscription;
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_leak);
mSubscription = Observable.interval(1, TimeUnit.SECONDS)
.subscribe(new Action1<Long>() {
@Override public void call(Long aLong) {
// pass
}
});
}
@Override protected void onDestroy() {
super.onDestroy();
mSubscription.unsubscribe();
}
}
三、內(nèi)存泄漏的檢測(cè)
觀察 Memory Monitor 內(nèi)存走勢(shì)圖,可以或多或少知道內(nèi)存情況,但如果要精確地追蹤到內(nèi)存泄漏點(diǎn),這里特別推薦偉大的 Square 公司開源的 LeakCanary 方案,LeakCanary 可以做到非常簡單方便、低侵入性地捕獲內(nèi)存泄漏代碼,甚至很多時(shí)候你可以捕捉到 Android 官方組件的內(nèi)存泄漏代碼。
參考利用 LeakCanary 來檢查 Android 內(nèi)存泄漏