內(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ì)的檢查,很容易解決并防范于未然嘿架。