如何防止內(nèi)存泄漏

內(nèi)存泄露

說到內(nèi)存泄露,就不得不提到內(nèi)存溢出,這兩個比較容易混淆的概念,我們來分析一下。

內(nèi)存泄露程序在向系統(tǒng)申請分配內(nèi)存空間后(new),在使用完畢后未釋放。結(jié)果導(dǎo)致一直占據(jù)該內(nèi)存單元,我們和程序都無法再使用該內(nèi)存單元,直到程序結(jié)束,這是內(nèi)存泄露。

內(nèi)存溢出程序向系統(tǒng)申請的內(nèi)存空間超出了系統(tǒng)能給的。比如內(nèi)存只能分配一個int類型,我卻要塞給他一個long類型,系統(tǒng)就出現(xiàn)oom。又比如一車最多能坐5個人,你卻非要塞下10個,車就擠爆了。

大量的內(nèi)存泄露會導(dǎo)致內(nèi)存溢出(oom)。

內(nèi)存

想要了解內(nèi)存泄露,對內(nèi)存的了解必不可少。

JAVA是在JVM所虛擬出的內(nèi)存環(huán)境中運行的,JVM的內(nèi)存可分為三個區(qū):堆(heap)、棧(stack)和方法區(qū)(method)。

棧(stack):是簡單的數(shù)據(jù)結(jié)構(gòu),但在計算機中使用廣泛。棧最顯著的特征是:LIFO(Last In, First Out, 后進先出)。比如我們往箱子里面放衣服,先放入的在最下方,只有拿出后來放入的才能拿到下方的衣服。棧中只存放基本類型和對象的引用(不是對象)。

堆(heap)堆內(nèi)存用于存放由new創(chuàng)建的對象和數(shù)組。在堆中分配的內(nèi)存,由java虛擬機自動垃圾回收器來管理。JVM只有一個堆區(qū)(heap)被所有線程共享,堆中不存放基本類型和對象引用,只存放對象本身。

方法區(qū)(method):又叫靜態(tài)區(qū),跟堆一樣,被所有的線程共享。方法區(qū)包含所有的class和static變量。

內(nèi)存的概念大概理解清楚后,要考慮的問題來了:

到底是哪里的內(nèi)存會讓我們造成內(nèi)存泄露?

內(nèi)存泄露原因分析

在JAVA中JVM的棧記錄了方法的調(diào)用,每個線程擁有一個棧。在線程的運行過程當(dāng)中,執(zhí)行到一個新的方法調(diào)用,就在棧中增加一個內(nèi)存單元,即幀(frame)。在frame中,保存有該方法調(diào)用的參數(shù)、局部變量和返回地址。然而JAVA中的局部變量只能是基本類型變量(int),或者對象的引用。所以在棧中只存放基本類型變量和對象的引用。引用的對象保存在堆中。

當(dāng)某方法運行結(jié)束時,該方法對應(yīng)的frame將會從棧中刪除,frame中所有局部變量和參數(shù)所占有的空間也隨之釋放。線程回到原方法繼續(xù)執(zhí)行,當(dāng)所有的棧都清空的時候,程序也就隨之運行結(jié)束。

而對于堆內(nèi)存,堆存放著普通變量。在JAVA中堆內(nèi)存不會隨著方法的結(jié)束而清空,所以在方法中定義了局部變量,在方法結(jié)束后變量依然存活在堆中。

綜上所述,棧(stack)可以自行清除不用的內(nèi)存空間。但是如果我們不停的創(chuàng)建新對象,堆(heap)的內(nèi)存空間就會被消耗盡。所以JAVA引入了垃圾回收(garbage collection,簡稱GC)去處理堆內(nèi)存的回收,但如果對象一直被引用無法被回收,造成內(nèi)存的浪費,無法再被使用。所以對象無法被GC回收就是造成內(nèi)存泄露的原因!

垃圾回收機制

垃圾回收(garbage collection,簡稱GC)可以自動清空堆中不再使用的對象。在JAVA中對象是通過引用使用的。如果再沒有引用指向該對象,那么該對象就無從處理或調(diào)用該對象,這樣的對象稱為不可到達(unreachable)。垃圾回收用于釋放不可到達的對象所占據(jù)的內(nèi)存。

實現(xiàn)思想:我們將棧定義為root,遍歷棧中所有的對象的引用,再遍歷一遍堆中的對象。因為棧中的對象的引用執(zhí)行完畢就刪除,所以我們就可以通過棧中的對象的引用,查找到堆中沒有被指向的對象,這些對象即為不可到達對象,對其進行垃圾回收。

內(nèi)存泄露原因

如果持有對象的強引用,垃圾回收器是無法在內(nèi)存中回收這個對象。

內(nèi)存泄露的真因是:持有對象的強引用,且沒有及時釋放,進而造成內(nèi)存單元一直被占用,浪費空間,甚至可能造成內(nèi)存溢出!

其實在Android中會造成內(nèi)存泄露的情景無外乎兩種:

全局進程(process-global)的static變量。這個無視應(yīng)用的狀態(tài),持有Activity的強引用的怪物。

活在Activity生命周期之外的線程。沒有清空對Activity的強引用。

檢查一下你的項目中是否有以下幾種情況:

Static Activities

Static Views

Inner Classes

Anonymous Classes

Handler

Threads

TimerTask

Sensor Manager

推薦一個可檢測app內(nèi)存泄露的項目:LeakCanary(可以檢測app的內(nèi)存泄露)


八種容易發(fā)生內(nèi)存泄漏的代碼與對策

其中,尤其嚴重的是泄漏Activity對象,因為它占用了大量系統(tǒng)內(nèi)存。不管內(nèi)存泄漏的代碼表現(xiàn)形式如何,其核心問題在于:

在Activity生命周期之外仍持有其引用。

幸運的是,一旦泄漏發(fā)生且被定位到了,修復(fù)方法是相當(dāng)簡單的。

Static Actitivities

這種泄漏

private static MainActivity activity;

void setStaticActivity() {

activity = this;

}

構(gòu)造靜態(tài)變量持有Activity對象很容易造成內(nèi)存泄漏,因為靜態(tài)變量是全局存在的,所以當(dāng)MainActivity生命周期結(jié)束時,引用仍被持有。這種寫法開發(fā)者是有理由來使用的,所以我們需要正確的釋放引用讓垃圾回收機制在它被銷毀的同時將其回收。

Android提供了特殊的Set集合https://developer.android.com/reference/java/lang/ref/package-summary.html#classes

允許開發(fā)者控制引用的“強度”。Activity對象泄漏是由于需要被銷毀時,仍然被強引用著,只要強引用存在就無法被回收。

可以用弱引用代替強引用。

https://developer.android.com/reference/java/lang/ref/WeakReference.html.

弱引用不會阻止對象的內(nèi)存釋放,所以即使有弱引用的存在,該對象也可以被回收。

private static WeakReference activityReference;

void setStaticActivity() {

activityReference = new WeakReference(this);

}

Static Views

靜態(tài)變量持有View

private static View view;

void setStaticView() {

view = findViewById(R.id.sv_button);

}

由于View持有其宿主Activity的引用,導(dǎo)致的問題與Activity一樣嚴重。弱引用是個有效的解決方法,然而還有另一種方法是在生命周期結(jié)束時清除引用,Activity#onDestory()方法就很適合把引用置空。

private static View view;

@Override

public void onDestroy() {

super.onDestroy();

if (view != null) {

unsetStaticView();

}

}

void unsetStaticView() {

view = null;

}

Inner Class

這種泄漏

private static Object inner;

void createInnerClass() {

class InnerClass {

}

inner = new InnerClass();

}

與上述兩種情況相似,開發(fā)者必須注意用非靜態(tài)內(nèi)部類,因為非靜態(tài)內(nèi)部類持有外部類的隱式引用,容易導(dǎo)致意料之外的泄漏。然而內(nèi)部類可以訪問外部類的私有變量,只要我們注意引用的生命周期,就可以避免意外的發(fā)生。

避免靜態(tài)變量

這樣持有內(nèi)部類的成員變量是可以的。

private Object inner;

void createInnerClass() {

class InnerClass {

}

inner = new InnerClass();

}

Anonymous Classes

前面我們看到的都是持有全局生命周期的靜態(tài)成員變量引起的,直接或間接通過鏈?zhǔn)揭肁ctivity導(dǎo)致的泄漏。這次我們用AsyncTask

void startAsyncTask() {

new AsyncTask() {

@Override protected Void doInBackground(Void... params) {

while(true);

}

}.execute();

}

Handler

void createHandler() {

new Handler() {

@Override public void handleMessage(Message message) {

super.handleMessage(message);

}

}.postDelayed(new Runnable() {

@Override public void run() {

while(true);

}

}, Long.MAX_VALUE >> 1);

}

Thread

void scheduleTimer() {

new Timer().schedule(new TimerTask() {

@Override

public void run() {

while(true);

}

}, Long.MAX_VALUE >> 1);

}

全部都是因為匿名類導(dǎo)致的。匿名類是特殊的內(nèi)部類——寫法更為簡潔。當(dāng)需要一次性特殊的子類時,Java提供的語法糖能讓表達式最少化。這種很贊很偷懶的寫法容易導(dǎo)致泄漏。正如使用內(nèi)部類一樣,只要不跨越生命周期,內(nèi)部類是完全沒問題的。但是,這些類是用于產(chǎn)生后臺線程的,這些Java線程是全局的,而且持有創(chuàng)建者的引用(即匿名類的引用),而匿名類又持有外部類的引用。線程是可能長時間運行的,所以一直持有Activity的引用導(dǎo)致當(dāng)銷毀時無法回收。

這次我們不能通過移除靜態(tài)成員變量解決,因為線程是于應(yīng)用生命周期相關(guān)的。為了避免泄漏,我們必須舍棄簡潔偷懶的寫法,把子類聲明為靜態(tài)內(nèi)部類。

靜態(tài)內(nèi)部類不持有外部類的引用,打破了鏈?zhǔn)揭谩?/p>

所以對于AsyncTask

private static class NimbleTask extends AsyncTask {

@Override protected Void doInBackground(Void... params) {

while(true);

}

}

void startAsyncTask() {

new NimbleTask().execute();

}

Handler

private static class NimbleHandler extends Handler {

@Override public void handleMessage(Message message) {

super.handleMessage(message);

}

}

private static class NimbleRunnable implements Runnable {

@Override public void run() {

while(true);

}

}

void createHandler() {

new NimbleHandler().postDelayed(new NimbleRunnable(), Long.MAX_VALUE >> 1);

}

TimerTask

private static class NimbleTimerTask extends TimerTask {

@Override public void run() {

while(true);

}

}

void scheduleTimer() {

new Timer().schedule(new NimbleTimerTask(), Long.MAX_VALUE >> 1);

}

但是,如果你堅持使用匿名類,只要在生命周期結(jié)束時中斷線程就可以。

private Thread thread;

@Override

public void onDestroy() {

super.onDestroy();

if (thread != null) {

thread.interrupt();

}

}

void spawnThread() {

thread = new Thread() {

@Override public void run() {

while (!isInterrupted()) {

}

}

}

thread.start();

}

Sensor Manager

這種泄漏

void registerListener() {

SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);

Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);

sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST);

}

使用Android系統(tǒng)服務(wù)不當(dāng)容易導(dǎo)致泄漏,為了Activity與服務(wù)交互,我們把Activity作為監(jiān)聽器,引用鏈在傳遞事件和回調(diào)中形成了。只要Activity維持注冊監(jiān)聽狀態(tài),引用就會一直持有,內(nèi)存就不會被釋放。

在Activity結(jié)束時注銷監(jiān)聽器

private SensorManager sensorManager;

private Sensor sensor;

@Override

public void onDestroy() {

super.onDestroy();

if (sensor != null) {

unregisterListener();

}

}

void unregisterListener() {

sensorManager.unregisterListener(this, sensor);

}

總結(jié)

Activity泄漏的案例我們已經(jīng)都走過一遍了,其他都大同小異。建議日后遇到類似的情況時,就使用相應(yīng)的解決方法。內(nèi)存泄漏只要發(fā)生過一次,通過詳細的檢查,很容易解決并防范于未然。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容