說(shuō)明
請(qǐng)尊重原創(chuàng),本文轉(zhuǎn)自:https://blog.csdn.net/u013495603/article/details/50696170焕檬,
一直很想總結(jié)內(nèi)存泄漏的成因和解決辦法,發(fā)現(xiàn)有人已經(jīng)總結(jié)得很全面痕囱,于是直接轉(zhuǎn)載,為了尊重作者,原文中發(fā)現(xiàn)的非代碼錯(cuò)誤之處只加以標(biāo)注不會(huì)直接修改趋观,代碼部分做了些必要的修改和排版以及修訂,本文只做學(xué)習(xí)總結(jié)之用锋边,侵刪!
前言
內(nèi)存管理的目的就是讓我們?cè)陂_(kāi)發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問(wèn)題编曼。內(nèi)存泄漏大家都不陌生了豆巨,簡(jiǎn)單粗俗的講,就是該被釋放的對(duì)象沒(méi)有釋放掐场,一直被某個(gè)或某些實(shí)例所持有卻不再被使用導(dǎo)致GC不能回收往扔。最近自己閱讀了大量相關(guān)的文檔資料,打算做個(gè)總結(jié)熊户,沉淀下來(lái)跟大家一起分享和學(xué)習(xí)萍膛,也給自己一個(gè)警示,以后coding時(shí)怎么避免這些情況嚷堡,提高應(yīng)用的體驗(yàn)和質(zhì)量蝗罗。
我會(huì)從Java內(nèi)存泄漏的基礎(chǔ)知識(shí)開(kāi)始,并通過(guò)具體例子來(lái)說(shuō)明Android引起內(nèi)存泄漏的各種原因蝌戒,以及如何利用工具來(lái)分析應(yīng)用內(nèi)存泄漏串塑,最后再做總結(jié),
篇幅有些長(zhǎng)北苟,大家可以分幾節(jié)來(lái)看桩匪!
Java內(nèi)存分配策略
java程序運(yùn)行時(shí)的內(nèi)存分配策略有三種,分別是靜態(tài)分配友鼻,棧式分配和堆式分配傻昙,對(duì)應(yīng)的闺骚,三種存儲(chǔ)策略使用的內(nèi)存空間主要分別是存儲(chǔ)區(qū)(也稱方法區(qū))、棧區(qū)和堆區(qū)妆档。
靜態(tài)存儲(chǔ)區(qū)(方法區(qū)):主要存放靜態(tài)數(shù)據(jù)僻爽、全局static數(shù)據(jù)和常量。這塊內(nèi)存在程序編譯時(shí)就已經(jīng)分配好过吻,并且在整個(gè)程序運(yùn)行期間都存在进泼。
棧區(qū):當(dāng)方法被執(zhí)行時(shí),方法體內(nèi)的局部變量都在棧上創(chuàng)建纤虽,并在方法執(zhí)行結(jié)束時(shí)這些局部變量所持有的內(nèi)存將會(huì)自動(dòng)被釋放乳绕。因?yàn)闂?nèi)存分配運(yùn)算內(nèi)置于處理器的指令集中,效率很高逼纸,但是分配的內(nèi)存容量有限洋措。
堆區(qū):又稱動(dòng)態(tài)內(nèi)存分配,通常就是指在程序運(yùn)行時(shí)直接new出來(lái)的內(nèi)存杰刽。這部分內(nèi)存在不使用時(shí)將會(huì)由Java垃圾回收器來(lái)負(fù)責(zé)回收菠发。
棧與堆的區(qū)別:
在方法體內(nèi)定義的(局部變量)一些基本類型的變量和對(duì)象的引用變量都是在方法的棧內(nèi)存中分配的。當(dāng)在一段方法中定義一個(gè)變量時(shí)贺嫂,Java就會(huì)在棧中為該變量分配內(nèi)存空間滓鸠,當(dāng)超過(guò)變量的作用域后,該變量也就無(wú)效了第喳,分配給它的內(nèi)存空間也將被釋放掉糜俗,該內(nèi)存空間可以被重新使用。
堆內(nèi)存用來(lái)存放所有由new創(chuàng)建的對(duì)象(包括該對(duì)象其中的所有成員變量)和數(shù)組曲饱。在堆中分配的內(nèi)存悠抹,將由Java垃圾回收器來(lái)自動(dòng)管理。在堆中產(chǎn)生了一個(gè)數(shù)組或者對(duì)象后扩淀,還可以在棧中定義一個(gè)特殊的變量楔敌,這個(gè)變量的取值等于數(shù)組或者對(duì)象在堆內(nèi)存中的首地址,這個(gè)特殊的變量就是我們上面說(shuō)的引用變量驻谆。我們可以通過(guò)這個(gè)引用變量來(lái)訪問(wèn)堆中的對(duì)象或者數(shù)組卵凑。
舉個(gè)例子:
public class Sample{
int s1 = 0;
Sample mSample1 = new Sample();
public void method(){
int s2 = 1;
Sample mSample2 = new Sample();
}
}
Sample mSample3 = new Sample();
Sample類的局部變量s2和引用變量mSample2都是存在于棧中,但mSample2指向的對(duì)象是存在于堆上的胜臊。mSample3指向的對(duì)象實(shí)體存放在堆上氛谜,包括這個(gè)對(duì)象的所有成員s1和mSample1,而它自己存在于棧中区端。
結(jié)論:
局部變量的基本數(shù)據(jù)類型和引用存儲(chǔ)于棧中值漫,引用的對(duì)象實(shí)體存儲(chǔ)于堆中≈危——因?yàn)樗鼈儗儆诜椒ǖ闹械淖兞垦詈危芷陔S方法而結(jié)束酱塔。
成員變量全部存儲(chǔ)于堆中(包括基本數(shù)據(jù)類型,引用和引用的對(duì)象實(shí)體)——因?yàn)樗鼈儗儆陬愇J悓?duì)象終究是要被new出來(lái)使用的羊娃。了解了Java的分配之后,我們?cè)賮?lái)看看Java是怎么管理內(nèi)存的埃跷。注意:實(shí)際上蕊玷,對(duì)象不一定全是分配在堆中,涉及到JVM內(nèi)存分配策略和逃逸分析弥雹,推薦比較詳細(xì)的闡述文章:
對(duì)象并不一定都是在堆上分配內(nèi)存的
Java是如何管理內(nèi)存
Java的內(nèi)存管理就是對(duì)象的分配和釋放問(wèn)題垃帅。在Java中,程序員需要通過(guò)關(guān)鍵字new為每個(gè)對(duì)象申請(qǐng)內(nèi)存空間(基本類型除外)剪勿,所有的對(duì)象都在堆(Heap)中分配空間贸诚。另外,對(duì)象的釋放是由GC決定和執(zhí)行的厕吉。在Java中酱固,內(nèi)存的分配是由程序完成的,而內(nèi)存的釋放是由GC完成的头朱,這種收支兩條線的方法確實(shí)簡(jiǎn)化了程序員的工作运悲。但同時(shí),它也加重了JVM的工作项钮。也就是Java程序運(yùn)行速度較慢的原因之一班眯。因?yàn)椋珿C為了能夠正確釋放對(duì)象寄纵,GC必須監(jiān)控每一個(gè)對(duì)象的運(yùn)行狀態(tài),包括對(duì)象的申請(qǐng)脖苏、引用程拭、被引用、賦值等棍潘,GC都需要進(jìn)行監(jiān)控恃鞋。
監(jiān)控對(duì)象狀態(tài)是為了更加準(zhǔn)確地、及時(shí)地釋放對(duì)象亦歉,而釋放對(duì)象的根本原則就是該對(duì)象不再被引用恤浪。
為了更好的理解GC的工作原理,我們可以將對(duì)象考慮為有向圖的頂點(diǎn)肴楷,將引用關(guān)系考慮為圖的有向邊水由,有向邊從引用者指向被引用對(duì)象。另外赛蔫,每個(gè)線程對(duì)象可以作為一個(gè)圖的起始點(diǎn)砂客,例如大多程序從main進(jìn)程開(kāi)始執(zhí)行泥张,那么改圖就是以main進(jìn)程頂點(diǎn)開(kāi)始的一顆根樹(shù),在這個(gè)有向圖中鞠值,根頂點(diǎn)可達(dá)的對(duì)象都是有效對(duì)象媚创,GC將不回收這些對(duì)象。如果某個(gè)對(duì)象(連通子圖)與這個(gè)根頂點(diǎn)不可達(dá)(注意彤恶,該圖為有向圖)钞钙,那么我們認(rèn)為這個(gè)(這些)對(duì)象不再被引用,可以被GC回收声离。
以下芒炼,我們舉一個(gè)例子說(shuō)明如何用有向圖表示內(nèi)存管理。對(duì)于程序的每一個(gè)時(shí)刻抵恋,我們都有一個(gè)有向圖表示JVM的內(nèi)存分配情況焕议。以下右圖,就是左邊程序運(yùn)行到第6行的示意圖弧关。
Java使用有向圖的方法進(jìn)行內(nèi)存管理盅安,可以消除引用循環(huán)的問(wèn)題,例如有三個(gè)對(duì)象相互引用世囊,只要它們和根進(jìn)程不可達(dá)的别瞭,那么GC也是可以回收它們的。這種方式的優(yōu)點(diǎn)是管理內(nèi)存的精度很高株憾,但是效率較低蝙寨。另外一種常用的內(nèi)存管理技術(shù)是使用計(jì)數(shù)器,例如COM模型采用計(jì)數(shù)器方式管理構(gòu)件嗤瞎,它與有向圖相比墙歪,精度行低(很難處理循環(huán)引用的問(wèn)題),但執(zhí)行效率很高贝奇。
什么是Java中的內(nèi)存泄漏
在Java中虹菲,內(nèi)存泄漏就是存在一些被分配的對(duì)象,這些對(duì)象有下面兩個(gè)特點(diǎn)掉瞳,首先毕源,這些對(duì)象是可達(dá)的,即在有向圖中陕习,存在通路可以與其相連霎褐;其次,這些對(duì)象是無(wú)用的该镣,即程序以后不會(huì)再使用這些對(duì)象冻璃。如果對(duì)象滿足這兩個(gè)條件,這些對(duì)象就可以判定為Java中的內(nèi)存泄漏,這些對(duì)象不會(huì)被GC所回收俱饿,然后它卻占用內(nèi)存歌粥。
在C++中,內(nèi)存泄漏的范圍更大一些拍埠。有些對(duì)象被分配了內(nèi)存空間失驶,然后卻不可達(dá),由于C++中沒(méi)有GC枣购,這些內(nèi)存將擁有收不回來(lái)嬉探。在Java中,這些不可達(dá)的對(duì)象都由GC負(fù)責(zé)回收棉圈,因此程序猿不需要考慮這部分的內(nèi)存泄漏涩堤。
通過(guò)分析,我們得知分瘾,對(duì)應(yīng)C++胎围,程序員需要自己管理邊和頂點(diǎn),而對(duì)于Java程序員只需要管理邊就可以了(不需要管理頂點(diǎn)的釋放)德召。通過(guò)這種方式白魂,Java提高了編程的效率。
因此上岗,通過(guò)以上分析福荸,我們知道在Java中也有內(nèi)存泄漏,但范圍比C++要小一些肴掷。因?yàn)镴ava從語(yǔ)言上保證敬锐,任何對(duì)象都是可達(dá)的,所有的不可達(dá)對(duì)象都由GC管理呆瞻。
對(duì)于程序員來(lái)說(shuō)台夺,GC基本是透明的,不可見(jiàn)的痴脾。雖然颤介,我們只有幾個(gè)函數(shù)可以訪問(wèn)GC,例如運(yùn)行GC的函數(shù)System.gc()明郭,但是根據(jù)Java語(yǔ)言規(guī)范定義买窟, 該函數(shù)不保證JVM的垃圾收集器一定會(huì)執(zhí)行丰泊。因?yàn)槭矶ǎ煌腏VM實(shí)現(xiàn)者可能使用不同的算法管理GC。通常瞳购,GC的線程的優(yōu)先級(jí)別較低话侄。JVM調(diào)用GC的策略也有很多種,有的是內(nèi)存使用到達(dá)一定程度時(shí),GC才開(kāi)始工作年堆,也有定時(shí)執(zhí)行的吞杭,有的是平緩執(zhí)行GC,有的是中斷式執(zhí)行GC变丧。但通常來(lái)說(shuō)芽狗,我們不需要關(guān)心這些。除非在一些特定的場(chǎng)合痒蓬,GC的執(zhí)行影響應(yīng)用程序的性能童擎,例如對(duì)于基于Web的實(shí)時(shí)系統(tǒng),如網(wǎng)絡(luò)游戲等攻晒,用戶不希望GC突然中斷應(yīng)用程序執(zhí)行而進(jìn)行垃圾回收顾复,那么我們需要調(diào)整GC的參數(shù),讓GC能夠通過(guò)平緩的方式釋放內(nèi)存鲁捏,例如將垃圾回收分解為一系列的小步驟執(zhí)行芯砸,Sun提供的HotSpot JVM就支持這一特性。同樣給出一個(gè) Java 內(nèi)存泄漏的典型例子给梅,
Vector v = new Vector(10);
for(int i = 1; i < 100; i++){
Object o = new Object();
v.add(o);
o = null;
}
在這個(gè)例子中假丧,我們循環(huán)申請(qǐng)Object對(duì)象,并將所申請(qǐng)的對(duì)象放入一個(gè) Vector 中破喻,如果我們僅僅釋放引用本身虎谢,那么 Vector 仍然引用該對(duì)象,所以這個(gè)對(duì)象對(duì) GC 來(lái)說(shuō)是不可回收的曹质。因此婴噩,如果對(duì)象加入到Vector 后,還必須從 Vector 中刪除羽德,最簡(jiǎn)單的方法就是將 Vector 對(duì)象設(shè)置為 null几莽。
Android中常見(jiàn)的內(nèi)存泄漏匯總
- 集合類泄漏
集合類如果僅僅有添加元素的方法,而沒(méi)有相應(yīng)的刪除機(jī)制宅静,導(dǎo)致內(nèi)存被占用章蚣。如果這個(gè)集合類是全局性的變量 (比如類中的靜態(tài)屬性,全局性的 map 等即有靜態(tài)引用或 final 一直指向它)姨夹,那么沒(méi)有相應(yīng)的刪除機(jī)制纤垂,很可能導(dǎo)致集合所占用的內(nèi)存只增不減。比如上面的典型例子就是其中一種情況磷账,當(dāng)然實(shí)際上我們?cè)陧?xiàng)目中肯定不會(huì)寫這么爛的代碼峭沦,但稍不注意還是很容易出現(xiàn)這種情況,比如我們都喜歡通過(guò) HashMap 做一些緩存之類的事逃糟,這種情況就要多留一些心眼吼鱼。 - 單例造成的內(nèi)存泄漏
由于單例的靜態(tài)特性使得其生命周期跟應(yīng)用的生命周期一樣長(zhǎng)蓬豁,所以如果使用不恰當(dāng)?shù)脑挘苋菀自斐蓛?nèi)存泄漏菇肃。比如下面一個(gè)典型的例子地粪,
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;
}
}
這是一個(gè)普通的單例模式,當(dāng)創(chuàng)建這個(gè)單例的時(shí)候琐谤,由于需要傳入一個(gè)Context蟆技,所以這個(gè)Context的生命周期的長(zhǎng)短至關(guān)重要:
- 如果此時(shí)傳入的是 Application 的 Context,因?yàn)?Application 的生命周期就是整個(gè)應(yīng)用的生命周期斗忌,所以這將沒(méi)有任何問(wèn)題付魔。
- 如果此時(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)該改為下面這種方式:
public class AppManager {
private static AppManager instance;
private Context context;
private AppManager(Context context){
this.context = context.getApplicationContext(); //使用Application的context
}
public static AppManager getInstance(Context context) {
//原文是 != 這里改正下
if (instance == null){
instance = new AppManager(context);
}
return instance;
}
}
或者這樣寫,連 Context 都不用傳進(jìn)來(lái)了:
/**Application類*/
public class GlobalApplication extends Application {
private static Context context;
@Override
public void onCreate() {
super.onCreate();
context = getApplicationContext();
}
/**
* 獲取上下文對(duì)象
*
* @return context
*/
public static Context getContext() {
return context;
}
}
/**AppManager類*/
public class AppManager {
private static AppManager instance;
private Context context;
private AppManager(){
this.context = GlobalApplication.getContext(); //使用Application的context
}
public static AppManager getInstance() {
//原文是 != 這里改正下
if (instance == null){
instance = new AppManager();
}
return instance;
}
}
- 匿名內(nèi)部類/非靜態(tài)內(nèi)部類和異步線程
非靜態(tài)內(nèi)部類創(chuàng)建靜態(tài)實(shí)例造成的內(nèi)存泄漏
有的時(shí)候我們可能會(huì)在啟動(dòng)頻繁的Activity中惊窖,為了避免重復(fù)創(chuàng)建相同的數(shù)據(jù)資源刽宪,可能會(huì)出現(xiàn)這種寫法:
public class MainAvtivity extends AppCompatActivity{
private static TestResource mResource = null;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_activity);
if (mResource == null){
mResource = new TestResource();
}
}
class TestResource{
}
}
這樣就在Activity內(nèi)部創(chuàng)建了一個(gè)非靜態(tài)內(nèi)部類的單例,每次啟動(dòng)Activity時(shí)都會(huì)使用該單例的數(shù)據(jù)界酒,這樣雖然避免了資源的重復(fù)創(chuàng)建圣拄,不過(guò)這種寫法卻會(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)用的一樣長(zhǎng),這就導(dǎo)致了該靜態(tài)實(shí)例一直會(huì)持有該Activity的引用凭疮,導(dǎo)致Activity的內(nèi)存資源不能正撤苟回收。
正確的做法為:
將該內(nèi)部類設(shè)為靜態(tài)內(nèi)部類或?qū)⒃搩?nèi)部類抽取出來(lái)封裝成一個(gè)單例执解,如果需要使用Context寞肖,請(qǐng)按照上面推薦的使用Application 的 Context。當(dāng)然衰腌,Application 的 context 不是萬(wàn)能的新蟆,所以也不能隨便亂用,對(duì)于有些地方則必須使用 Activity 的 Context右蕊,對(duì)于Application琼稻,Service,Activity三者的Context的應(yīng)用場(chǎng)景如下:
功能 | Application | Service | Activity |
---|---|---|---|
Start an activity | NO1 | NO1 | YES |
Show a Dialog | NO | NO | YES |
Layout Inflation | YES | YES | YES |
Start a Service | YES | YES | YES |
Bind to a Service | YES | YES | YES |
Send a Broadcast | YES | YES | YES |
Register BroadcastReceiver | YES | YES | YES |
Load Resource Values | YES | YES | YES |
其中: NO1表示 Application 和 Service 可以啟動(dòng)一個(gè) Activity尤泽,不過(guò)需要?jiǎng)?chuàng)建一個(gè)新的 task 任務(wù)隊(duì)列欣簇。而對(duì)于 Dialog 而言,只有在 Activity 中才能創(chuàng)建
匿名內(nèi)部類
android開(kāi)發(fā)經(jīng)常會(huì)繼承實(shí)現(xiàn)Activity/Fragment/View坯约,此時(shí)如果你使用了匿名類熊咽,并被異步線程持有了,那要小心了闹丐,如果沒(méi)有任何措施這樣一定會(huì)導(dǎo)致泄露
public class MainActivity extends Activity{
Runnable ref1 = new MyRunnable();
Runnable ref2 = new Runnable() {
@Override
public void run() {
}
};
}
ref1和ref2的區(qū)別是横殴,ref2使用了匿名內(nèi)部類。我們來(lái)看看運(yùn)行時(shí)這兩個(gè)引用的內(nèi)存:
可以看到卿拴,ref1沒(méi)什么特別的衫仑。
但ref2這個(gè)匿名類的實(shí)現(xiàn)對(duì)象里面多了一個(gè)引用:
this$0這個(gè)引用指向MainActivity.this,也就是說(shuō)當(dāng)前的MainActivity實(shí)例會(huì)被ref2持有堕花,如果將這個(gè)引用再傳入一個(gè)異步線程文狱,此線程和此Acitivity生命周期不一致的時(shí)候,就造成了Activity的泄露缘挽。
避免此問(wèn)題瞄崇,請(qǐng)使用靜態(tài)內(nèi)部類/匿名類替換非靜態(tài)內(nèi)部類/匿名類
- Handler 造成的內(nèi)存泄漏
Handler 的使用造成的內(nèi)存泄漏問(wèn)題應(yīng)該說(shuō)是最為常見(jiàn)了,很多時(shí)候我們?yōu)榱吮苊?ANR 而不在主線程進(jìn)行耗時(shí)操作壕曼,在處理網(wǎng)絡(luò)任務(wù)或者封裝一些請(qǐng)求回調(diào)等api都借助Handler來(lái)處理苏研,但 Handler 不是萬(wàn)能的,對(duì)于 Handler 的使用代碼編寫一不規(guī)范即有可能造成內(nèi)存泄漏腮郊。另外摹蘑,我們知道 Handler、Message 和 MessageQueue 都是相互關(guān)聯(lián)在一起的轧飞,萬(wàn)一 Handler 發(fā)送的 Message 尚未被處理衅鹿,則該 Message 及發(fā)送它的 Handler 對(duì)象將被線程 MessageQueue 一直持有。
由于 Handler 屬于 TLS(Thread Local Storage) 變量, 生命周期和 Activity 是不一致的过咬。因此這種實(shí)現(xiàn)方式一般很難保證跟 View 或者 Activity 的生命周期保持一致塘安,故很容易導(dǎo)致無(wú)法正確釋放。
舉個(gè)例子:
public class SampleActivity extends Activity{
private Handler mLeakyHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
@Override
protected void onCreate(@Nullable 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 的生命周期就無(wú)關(guān)了诗芜。
同時(shí)通過(guò)弱引用的方式引入 Activity瞳抓,避免直接將 Activity 作為 context 傳進(jìn)去,見(jiàn)下面代碼:
static class MyHandler extends Handler
{
WeakReference<Activity> mWeakReference;
public MyHandler(Activity activity)
{
mWeakReference=new WeakReference<Activity>(activity);
}
@Override
public void handleMessage(Message msg)
{
final Activity activity=mWeakReference.get();
if(activity!=null)
{
//do something
}
}
綜述伏恐,即推薦使用靜態(tài)內(nèi)部類 + WeakReference 這種方式孩哑。每次使用前注意判空。
前面提到了 WeakReference翠桦,所以這里就簡(jiǎn)單的說(shuō)一下 Java 對(duì)象的幾種引用類型横蜒。
Java對(duì)引用的分類有 StrongReference, SoftReference, WeakReference, PhantomReference 四種。
類型 | 級(jí)別 | 回收機(jī)制 | 用途 | 生存時(shí)間 |
---|---|---|---|---|
StrongReference | 強(qiáng) | 從來(lái)不會(huì) | 對(duì)象的一般狀態(tài) | JVM停止運(yùn)行時(shí)終止 |
SoftReference | 軟 | 在內(nèi)存不足時(shí) | 聯(lián)合ReferenceQueue構(gòu)造有效期短/占用內(nèi)存大/生命周期長(zhǎng)的對(duì)象的二級(jí)高速緩沖器(內(nèi)存不足才清空) | 內(nèi)存不足時(shí)終止 |
WeakReference | 弱 | 在垃圾回收時(shí) | 聯(lián)合ReferenceQueue構(gòu)造有效期短/占用內(nèi)存大/生命周期長(zhǎng)的對(duì)象的一級(jí)高速緩沖器(系統(tǒng)發(fā)生gc則清空) | gc運(yùn)行后終止 |
PhantomReference | 虛 | 在垃圾回收時(shí) | 聯(lián)合ReferenceQueue跟蹤對(duì)象被垃圾回收器回收的活動(dòng) | gc運(yùn)行后終止 |
在Android應(yīng)用的開(kāi)發(fā)中销凑,為了防止內(nèi)存溢出丛晌,在處理一些占用內(nèi)存大而且聲明周期較長(zhǎng)的對(duì)象時(shí)候,可以盡量應(yīng)用軟引用和弱引用技術(shù)斗幼。
軟/弱引用可以和一個(gè)引用隊(duì)列(ReferenceQueue)聯(lián)合使用澎蛛,如果軟引用所引用的對(duì)象被垃圾回收器回收,Java虛擬機(jī)就會(huì)把這個(gè)軟引用加入到與之關(guān)聯(lián)的引用隊(duì)列中蜕窿。利用這個(gè)隊(duì)列可以得知被回收的軟/弱引用的對(duì)象列表瓶竭,從而為緩沖器清除已失效的軟/弱引用。假設(shè)我們的應(yīng)用會(huì)用到大量的默認(rèn)圖片渠羞,比如應(yīng)用中有默認(rèn)的頭像斤贰,默認(rèn)游戲圖標(biāo)等等,這些圖片很多地方會(huì)用到次询。如果每次都去讀取圖片荧恍,由于讀取文件需要硬件操作,速度較慢屯吊,會(huì)導(dǎo)致性能較低送巡。所以我們考慮將圖片緩存起來(lái),需要的時(shí)候直接從內(nèi)存中讀取盒卸。但是骗爆,由于圖片占用內(nèi)存空間比較大,緩存很多圖片需要很多的內(nèi)存蔽介,就可能比較容易發(fā)生OutOfMemory異常摘投。這時(shí),我們可以考慮使用軟/弱引用技術(shù)來(lái)避免這個(gè)問(wèn)題發(fā)生虹蓄。以下就是高速緩沖器的雛形: 首先定義一個(gè)HashMap犀呼,保存軟引用對(duì)象。
private Map<String, SoftReference<Bitmap>> imageCache = new HashMap<>();
再來(lái)定義一個(gè)方法薇组,保存Bitmap的軟引用到HashMap外臂。
public class CachBySoftRef{
//首先定義一個(gè)HashMap,保存軟引用對(duì)象
private Map<String, SoftReference<Bitmap>> imageCache = new HashMap<>();
//再來(lái)定義一個(gè)方法律胀,保存Bitmap的軟引用到HashMap.
public void addBitmapToCache(String path){
//強(qiáng)引用的Bitmap對(duì)象
Bitmap bitmap = BitmapFactory.decodeFile(path);
//軟引用的Bitmap對(duì)象
SoftReference<Bitmap> softBitmap = new SoftReference<>(bitmap);
//添加對(duì)象到Map中使其緩存
imageCache.put(path,softBitmap);
}
//獲取的時(shí)候宋光,可以通過(guò)SoftReference的get()方法得到Bitmap對(duì)象.
public Bitmap getBitmapByPath(String path){
//從緩存中取軟引用的Bitmap對(duì)象
SoftReference<Bitmap> softBitmap = imageCache.get(path);
//判斷是否存在引用
if (softBitmap == null){
return null;
}
//通過(guò)軟引用取出Bitmap對(duì)象貌矿,如果由于內(nèi)存不足Bitmap被回收,將取得空罪佳,如果未被回收逛漫,則可重復(fù)使用,提高速度.
Bitmap bitmap = softBitmap.get();
return bitmap;
}
}
使用軟引用以后菇民,在OutOfMemory異常發(fā)生之前,這些緩存的圖片資源的內(nèi)存空間可以被釋放掉的投储,從而避免內(nèi)存達(dá)到上限第练,避免Crash發(fā)生。
如果只是想避免OutOfMemory異常的發(fā)生玛荞,則可以使用軟引用娇掏。如果對(duì)于應(yīng)用的性能更在意,想盡快回收一些占用內(nèi)存比較大的對(duì)象勋眯,則可以使用弱引用婴梧。
另外可以根據(jù)對(duì)象是否經(jīng)常使用來(lái)判斷選擇軟引用還是弱引用。如果該對(duì)象可能會(huì)經(jīng)常使用的客蹋,就盡量用軟引用塞蹭。如果該對(duì)象不被使用的可能性更大些,就可以用弱引用讶坯。
ok番电,繼續(xù)回到主題。前面所說(shuō)的辆琅,創(chuàng)建一個(gè)靜態(tài)Handler內(nèi)部類漱办,然后對(duì) Handler 持有的對(duì)象使用弱引用,這樣在回收時(shí)也可以回收 Handler 持有的對(duì)象婉烟,但是這樣做雖然避免了 Activity 泄漏娩井,不過(guò) Looper 線程的消息隊(duì)列中還是可能會(huì)有待處理的消息,所以我們?cè)?Activity 的 Destroy 時(shí)或者 Stop 時(shí)應(yīng)該移除消息隊(duì)列 MessageQueue 中的消息似袁。
下面幾個(gè)方法都可以移除 Message:
/**
* Remove any pending posts of Runnable r that are in the message queue.
*/
public final void removeCallbacks(Runnable r)
{
mQueue.removeMessages(this, r, null);
}
/**
* Remove any pending posts of Runnable <var>r</var> with Object
* <var>token</var> that are in the message queue. If <var>token</var> is null,
* all callbacks will be removed.
*/
public final void removeCallbacks(Runnable r, Object token)
{
mQueue.removeMessages(this, r, token);
}
/**
* Remove any pending posts of callbacks and sent messages whose
* <var>obj</var> is <var>token</var>. If <var>token</var> is null,
* all callbacks and messages will be removed.
*/
public final void removeCallbacksAndMessages(Object token) {
mQueue.removeCallbacksAndMessages(this, token);
}
/**
* Remove any pending posts of messages with code 'what' that are in the
* message queue.
*/
public final void removeMessages(int what) {
mQueue.removeMessages(this, what, null);
}
/**
* Remove any pending posts of messages with code 'what' and whose obj is
* 'object' that are in the message queue. If <var>object</var> is null,
* all messages will be removed.
*/
public final void removeMessages(int what, Object object) {
mQueue.removeMessages(this, what, object);
}
盡量避免使用 static 成員變量
- 如果成員變量被聲明為 static洞辣,那我們都知道其生命周期將與整個(gè)app進(jìn)程生命周期一樣。
這會(huì)導(dǎo)致一系列問(wèn)題昙衅,如果你的app進(jìn)程設(shè)計(jì)上是長(zhǎng)駐內(nèi)存的屋彪,那即使app切到后臺(tái),這部分內(nèi)存也不會(huì)被釋放绒尊。按照現(xiàn)在手機(jī)app內(nèi)存管理機(jī)制畜挥,占內(nèi)存較大的后臺(tái)進(jìn)程將優(yōu)先回收,因?yàn)槿绻薬pp做過(guò)進(jìn)程互保庇て祝活蟹但,那會(huì)造成app在后臺(tái)頻繁重啟躯泰。當(dāng)手機(jī)安裝了你參與開(kāi)發(fā)的app以后一夜時(shí)間手機(jī)被消耗空了電量、流量华糖,你的app不得不被用戶卸載或者靜默麦向。
這里修復(fù)的方法是:
不要在類初始時(shí)初始化靜態(tài)成員】筒妫可以考慮lazy初始化诵竭。 架構(gòu)設(shè)計(jì)上要思考是否真的有必要這樣做,盡量避免兼搏。如果架構(gòu)需要這么設(shè)計(jì)卵慰,那么此對(duì)象的生命周期你有責(zé)任管理起來(lái)。
- 避免 override finalize()
1佛呻、finalize 方法被執(zhí)行的時(shí)間不確定裳朋,不能依賴與它來(lái)釋放緊缺的資源。時(shí)間不確定的原因是:虛擬機(jī)調(diào)用GC的時(shí)間不確定 吓著,F(xiàn)inalize daemon線程被調(diào)度到的時(shí)間不確定
2鲤嫡、finalize 方法只會(huì)被執(zhí)行一次,即使對(duì)象被復(fù)活绑莺,如果已經(jīng)執(zhí)行過(guò)了 finalize 方法暖眼,再次被 GC 時(shí)也不會(huì)再執(zhí)行了,原因是:
含有 finalize 方法的 object 是在 new 的時(shí)候由虛擬機(jī)生成了一個(gè) finalize reference 在來(lái)引用到該Object的纺裁,而在 finalize 方法執(zhí)行的時(shí)候罢荡,該 object 所對(duì)應(yīng)的 finalize Reference 會(huì)被釋放掉,即使在這個(gè)時(shí)候把該 object 復(fù)活(即用強(qiáng)引用引用住該 object )对扶,再第二次被 GC 的時(shí)候由于沒(méi)有了 finalize reference 與之對(duì)應(yīng)区赵,所以 finalize 方法不會(huì)再執(zhí)行。
3浪南、含有Finalize方法的object需要至少經(jīng)過(guò)兩輪GC才有可能被釋放笼才。 - 資源未關(guān)閉造成的內(nèi)存泄漏
對(duì)于使用了BroadcastReceiver,ContentObserver络凿,F(xiàn)ile骡送,游標(biāo) Cursor,Stream絮记,Bitmap等資源的使用摔踱,應(yīng)該在Activity銷毀時(shí)及時(shí)關(guān)閉或者注銷,否則這些資源將不會(huì)被回收怨愤,造成內(nèi)存泄漏派敷。 - 一些不良代碼造成的內(nèi)存壓力
有些代碼并不造成內(nèi)存泄露,但是它們,或是對(duì)沒(méi)使用的內(nèi)存沒(méi)進(jìn)行有效及時(shí)的釋放篮愉,或是沒(méi)有有效的利用已有的對(duì)象而是頻繁的申請(qǐng)新內(nèi)存腐芍。
比如: - Bitmap 沒(méi)調(diào)用 recycle()方法,對(duì)于 Bitmap 對(duì)象在不使用時(shí),我們應(yīng)該先調(diào)用 recycle() 釋放內(nèi)存试躏,然后才它設(shè)置為 null. 因?yàn)榧虞d Bitmap 對(duì)象的內(nèi)存空間猪勇,一部分是 java 的,一部分 C 的(因?yàn)?Bitmap 分配的底層是通過(guò) JNI 調(diào)用的 )颠蕴。而這個(gè) recyle() 就是針對(duì) C 部分的內(nèi)存釋放泣刹。
- 構(gòu)造 Adapter 時(shí),沒(méi)有使用緩存的 convertView ,每次都在創(chuàng)建新的 converView犀被。這里推薦使用 ViewHolder椅您。
總結(jié)
- 對(duì) Activity 等組件的引用應(yīng)該控制在 Activity 的生命周期之內(nèi); 如果不能就考慮使用 getApplicationContext 或者 getApplication弱判,以避免 Activity 被外部長(zhǎng)生命周期的對(duì)象引用而泄露襟沮。
- 盡量不要在靜態(tài)變量或者靜態(tài)內(nèi)部類中使用非靜態(tài)外部成員變量(包括context )锥惋,即使要使用昌腰,也要考慮適時(shí)把外部成員變量置空;也可以在內(nèi)部類中使用弱引用來(lái)引用外部類的變量膀跌。
- 對(duì)于生命周期比Activity長(zhǎng)的內(nèi)部類對(duì)象遭商,并且內(nèi)部類中使用了外部類的成員變量,可以這樣做避免內(nèi)存泄漏:
將內(nèi)部類改為靜態(tài)內(nèi)部類
靜態(tài)內(nèi)部類中使用弱引用來(lái)引用外部類的成員變量 - Handler 的持有的引用對(duì)象最好使用弱引用捅伤,資源釋放時(shí)也可以清空 Handler 里面的消息劫流。比如在 Activity onStop 或者 onDestroy 的時(shí)候,取消掉該 Handler 對(duì)象的 Message和 Runnable.
- 在 Java 的實(shí)現(xiàn)過(guò)程中丛忆,也要考慮其對(duì)象釋放祠汇,最好的方法是在不使用某對(duì)象時(shí),顯式地將此對(duì)象賦值為 null熄诡,比如使用完Bitmap 后先調(diào)用 recycle()可很,再賦為null,清空對(duì)圖片等資源有直接引用或者間接引用的數(shù)組(使用 array.clear() ; array = null)等,最好遵循誰(shuí)創(chuàng)建誰(shuí)釋放的原則凰浮。
- 正確關(guān)閉資源我抠,對(duì)于使用了BraodcastReceiver,ContentObserver袜茧,F(xiàn)ile菜拓,游標(biāo) Cursor,Stream笛厦,Bitmap等資源的使用纳鼎,應(yīng)該在Activity銷毀時(shí)及時(shí)關(guān)閉或者注銷。
- 保持對(duì)對(duì)象生命周期的敏感,特別注意單例喷橙、靜態(tài)對(duì)象啥么、全局性集合等的生命周期。