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

內(nèi)存泄露

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

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

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

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

內(nèi)存

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

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

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

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

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

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

到底是哪里的內(nèi)存會(huì)讓我們?cè)斐蓛?nèi)存泄露浙炼?

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

在JAVA中JVM的棧記錄了方法的調(diào)用份氧,每個(gè)線程擁有一個(gè)棧。在線程的運(yùn)行過程當(dāng)中弯屈,執(zhí)行到一個(gè)新的方法調(diào)用蜗帜,就在棧中增加一個(gè)內(nèi)存單元,即幀(frame)季俩。在frame中钮糖,保存有該方法調(diào)用的參數(shù)、局部變量和返回地址酌住。然而JAVA中的局部變量只能是基本類型變量(int)店归,或者對(duì)象的引用。所以在棧中只存放基本類型變量和對(duì)象的引用酪我。引用的對(duì)象保存在堆中消痛。

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

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

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

垃圾回收機(jī)制

垃圾回收(garbage collection递鹉,簡(jiǎn)稱GC)可以自動(dòng)清空堆中不再使用的對(duì)象。在JAVA中對(duì)象是通過引用使用的藏斩。如果再?zèng)]有引用指向該對(duì)象,那么該對(duì)象就無從處理或調(diào)用該對(duì)象却盘,這樣的對(duì)象稱為不可到達(dá)(unreachable)狰域。垃圾回收用于釋放不可到達(dá)的對(duì)象所占據(jù)的內(nèi)存。

實(shí)現(xiàn)思想:我們將棧定義為root黄橘,遍歷棧中所有的對(duì)象的引用兆览,再遍歷一遍堆中的對(duì)象。因?yàn)闂V械膶?duì)象的引用執(zhí)行完畢就刪除塞关,所以我們就可以通過棧中的對(duì)象的引用抬探,查找到堆中沒有被指向的對(duì)象,這些對(duì)象即為不可到達(dá)對(duì)象帆赢,對(duì)其進(jìn)行垃圾回收小压。

內(nèi)存泄露原因

如果持有對(duì)象的強(qiáng)引用,垃圾回收器是無法在內(nèi)存中回收這個(gè)對(duì)象椰于。

內(nèi)存泄露的真因是:持有對(duì)象的強(qiáng)引用怠益,且沒有及時(shí)釋放,進(jìn)而造成內(nèi)存單元一直被占用瘾婿,浪費(fèi)空間蜻牢,甚至可能造成內(nèi)存溢出!

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

全局進(jìn)程(process-global)的static變量偏陪。這個(gè)無視應(yīng)用的狀態(tài)抢呆,持有Activity的強(qiáng)引用的怪物。

活在Activity生命周期之外的線程笛谦。沒有清空對(duì)Activity的強(qiáng)引用抱虐。

檢查一下你的項(xiàng)目中是否有以下幾種情況:

Static Activities

Static Views

Inner Classes

Anonymous Classes

Handler

Threads

TimerTask

Sensor Manager

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


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

其中,尤其嚴(yán)重的是泄漏Activity對(duì)象揪罕,因?yàn)樗加昧舜罅肯到y(tǒng)內(nèi)存梯码。不管內(nèi)存泄漏的代碼表現(xiàn)形式如何宝泵,其核心問題在于:

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

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

Static Actitivities

這種泄漏

private static MainActivity activity;

void setStaticActivity() {

activity = this;

}

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

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

允許開發(fā)者控制引用的“強(qiáng)度”贤重。Activity對(duì)象泄漏是由于需要被銷毀時(shí)茬祷,仍然被強(qiáng)引用著,只要強(qiáng)引用存在就無法被回收并蝗。

可以用弱引用代替強(qiáng)引用祭犯。

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

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

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一樣嚴(yán)重键畴。弱引用是個(gè)有效的解決方法最盅,然而還有另一種方法是在生命周期結(jié)束時(shí)清除引用,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)部類,因?yàn)?a href="https://https://link.jianshu.com/?t=https://docs.oracle.com/javase/tutorial/java/javaOO/nested.html" target="_blank">非靜態(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);

}

全部都是因?yàn)?a href="https://https://link.jianshu.com/?t=https://docs.oracle.com/javase/tutorial/java/javaOO/anonymousclasses.html" target="_blank">匿名類導(dǎo)致的。匿名類是特殊的內(nèi)部類——寫法更為簡(jiǎn)潔悲柱。當(dāng)需要一次性特殊的子類時(shí)锋喜,Java提供的語法糖能讓表達(dá)式最少化。這種很贊很偷懶的寫法容易導(dǎo)致泄漏。正如使用內(nèi)部類一樣嘿般,只要不跨越生命周期段标,內(nèi)部類是完全沒問題的。但是炉奴,這些類是用于產(chǎn)生后臺(tái)線程的逼庞,這些Java線程是全局的,而且持有創(chuàng)建者的引用(即匿名類的引用)瞻赶,而匿名類又持有外部類的引用赛糟。線程是可能長(zhǎng)時(shí)間運(yùn)行的,所以一直持有Activity的引用導(dǎo)致當(dāng)銷毀時(shí)無法回收砸逊。

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

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

所以對(duì)于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ān)持使用匿名類,只要在生命周期結(jié)束時(shí)中斷線程就可以遗淳。

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維持注冊(cè)監(jiān)聽狀態(tài)脂男,引用就會(huì)一直持有养叛,內(nèi)存就不會(huì)被釋放。

在Activity結(jié)束時(shí)注銷監(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)都走過一遍了宰翅,其他都大同小異弃甥。建議日后遇到類似的情況時(shí),就使用相應(yīng)的解決方法汁讼。內(nèi)存泄漏只要發(fā)生過一次淆攻,通過詳細(xì)的檢查,很容易解決并防范于未然嘿架。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末瓶珊,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子耸彪,更是在濱河造成了極大的恐慌伞芹,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,427評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異唱较,居然都是意外死亡扎唾,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門南缓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來胸遇,“玉大人,你說我怎么就攤上這事西乖『疲” “怎么了?”我有些...
    開封第一講書人閱讀 165,747評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵获雕,是天一觀的道長(zhǎng)薄腻。 經(jīng)常有香客問我,道長(zhǎng)届案,這世上最難降的妖魔是什么庵楷? 我笑而不...
    開封第一講書人閱讀 58,939評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮楣颠,結(jié)果婚禮上尽纽,老公的妹妹穿的比我還像新娘。我一直安慰自己童漩,他們只是感情好弄贿,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著矫膨,像睡著了一般差凹。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上侧馅,一...
    開封第一講書人閱讀 51,737評(píng)論 1 305
  • 那天危尿,我揣著相機(jī)與錄音,去河邊找鬼馁痴。 笑死谊娇,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的罗晕。 我是一名探鬼主播济欢,決...
    沈念sama閱讀 40,448評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼小渊!你這毒婦竟也來了船逮?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,352評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤粤铭,失蹤者是張志新(化名)和其女友劉穎挖胃,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,834評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡酱鸭,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,992評(píng)論 3 338
  • 正文 我和宋清朗相戀三年吗垮,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了拍冠。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片羡宙。...
    茶點(diǎn)故事閱讀 40,133評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖易阳,靈堂內(nèi)的尸體忽然破棺而出蔚舀,到底是詐尸還是另有隱情饵沧,我是刑警寧澤,帶...
    沈念sama閱讀 35,815評(píng)論 5 346
  • 正文 年R本政府宣布赌躺,位于F島的核電站狼牺,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏礼患。R本人自食惡果不足惜是钥,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,477評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望缅叠。 院中可真熱鬧悄泥,春花似錦、人聲如沸肤粱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽领曼。三九已至余寥,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間悯森,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工绪撵, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留瓢姻,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,398評(píng)論 3 373
  • 正文 我出身青樓音诈,卻偏偏與公主長(zhǎng)得像幻碱,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子细溅,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評(píng)論 2 355

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