OOM(OutOfMemory)
就是我們平時(shí)所碰到的內(nèi)存溢出,而內(nèi)存泄漏的最終后果就是導(dǎo)致OOM吻贿。
內(nèi)存泄漏是造成應(yīng)用程序OOM的主要原因之一臣咖!我們知道Android系統(tǒng)為每個(gè)應(yīng)用程序分配的內(nèi)存有限跨琳,而當(dāng)一個(gè)應(yīng)用中產(chǎn)生的內(nèi)存泄漏比較多時(shí)近顷,這就難免會(huì)導(dǎo)致應(yīng)用所需要的內(nèi)存超過(guò)這個(gè)系統(tǒng)分配的內(nèi)存限額诈唬,這就造成了內(nèi)存溢出而導(dǎo)致應(yīng)用Crash韩脏。
一、內(nèi)存分配策略
程序運(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ū):在執(zhí)行函數(shù)時(shí)界轩,函數(shù)內(nèi)局部變量的存儲(chǔ)單元都可以在棧上創(chuàng)建,函數(shù)執(zhí)行結(jié)束時(shí)這些存儲(chǔ)單元自動(dòng)被釋放衔瓮。
棧內(nèi)存分配運(yùn)算內(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)厥掌鳎┌帧?dòng)態(tài)內(nèi)存的生存期可以由我們決定幅疼,如果我們不釋放內(nèi)存,程序?qū)⒃谧詈蟛裴尫诺魟?dòng)態(tài)內(nèi)存昼接。 但是爽篷,良好的編程習(xí)慣是:如果某動(dòng)態(tài)內(nèi)存不再使用,需要將其釋放掉慢睡。
堆和棧的區(qū)別:
-
棧內(nèi)存
在函數(shù)中(說(shuō)明是局部變量)定義的一些基本類型的變量和對(duì)象的引用變量都是在函數(shù)的棧內(nèi)存中分配逐工。當(dāng)在一段代碼塊中定義一個(gè)變量時(shí),java就在棧中為這個(gè)變量分配內(nèi)存空間漂辐,當(dāng)超過(guò)變量的作用域后泪喊,java會(huì)自動(dòng)釋放掉為該變量分配的內(nèi)存空間,該內(nèi)存空間可以立刻被另作他用髓涯。 -
堆內(nèi)存
堆內(nèi)存用于存放所有由new創(chuàng)建的對(duì)象(內(nèi)容包括該對(duì)象其中的所有成員變量)和數(shù)組袒啼。在堆中分配的內(nèi)存,由java虛擬機(jī)自動(dòng)垃圾回收器來(lái)管理。在堆中產(chǎn)生了一個(gè)數(shù)組或者對(duì)象后蚓再,還可以在棧中定義一個(gè)特殊的變量滑肉,這個(gè)變量的取值等于數(shù)組或者對(duì)象在堆內(nèi)存中的首地址,在棧中的這個(gè)特殊的變量就變成了數(shù)組或者對(duì)象的引用變量摘仅,以后就可以在程序中使用棧內(nèi)存中的引用變量來(lái)訪問(wèn)堆中的數(shù)組或者對(duì)象靶庙,引用變量相當(dāng)于為數(shù)組或者對(duì)象起的一個(gè)別名,或者代號(hào)娃属。
堆是不連續(xù)的內(nèi)存區(qū)域(因?yàn)橄到y(tǒng)是用鏈表來(lái)存儲(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ì)造成大量?jī)?nèi)存碎片族阅,使程序效率降低篓跛。
對(duì)于棧,它是先進(jìn)后出的隊(duì)列坦刀,進(jìn)出一一對(duì)應(yīng)愧沟,不產(chǎn)生碎片,運(yùn)行效率穩(wěn)定高鲤遥。
結(jié)論:
- 局部變量的基本數(shù)據(jù)類型和引用存儲(chǔ)于棧中沐寺,引用的對(duì)象實(shí)體存儲(chǔ)于堆中。
——因?yàn)樗鼈儗儆诜椒ㄖ械淖兞扛悄危芷陔S方法而結(jié)束混坞。 - 成員變量全部存儲(chǔ)與堆中(包括基本數(shù)據(jù)類型,引用和引用的對(duì)象實(shí)體)钢坦。
——因?yàn)樗鼈儗儆陬惥吭校悓?duì)象終究是要被new出來(lái)使用的。
二爹凹、為什么會(huì)產(chǎn)生內(nèi)存泄漏厨诸?
這里所說(shuō)的內(nèi)存泄露只針對(duì)堆內(nèi)存,他們存放的就是引用指向的對(duì)象實(shí)體禾酱。為了判斷Java中是否有內(nèi)存泄露微酬,我們首先必須了解Java是如何管理(堆)內(nèi)存的绘趋。Java的內(nèi)存管理就是對(duì)象的分配和釋放問(wèn)題。在Java中得封,內(nèi)存的分配是由程序完成的,而內(nèi)存的釋放是由垃圾收集器(Garbage Collection指郁,GC)完成的忙上,程序員不需要通過(guò)調(diào)用函數(shù)來(lái)釋放內(nèi)存,但它只能回收無(wú)用并且不再被其它對(duì)象引用的那些對(duì)象所占用的空間闲坎。但當(dāng)一個(gè)對(duì)象已經(jīng)不需要再使用了疫粥,本該被回收時(shí),而有另外一個(gè)正在使用的對(duì)象持有它的引用從而導(dǎo)致它不能被回收腰懂,這導(dǎo)致本該被回收的對(duì)象不能被回收而停留在堆內(nèi)存中梗逮,這就產(chǎn)生了內(nèi)存泄漏。
三绣溜、Android中常見(jiàn)的導(dǎo)致內(nèi)存泄漏原因分析
-
單例模式
不正確使用單例模式是引起內(nèi)存泄露的一個(gè)常見(jiàn)問(wèn)題慷彤,單例對(duì)象在被初始化后將在 JVM 的整個(gè)生命周期中存在(以靜態(tài)變量的方式),如果單例對(duì)象持有外部對(duì)象的引用怖喻,那么這個(gè)外部對(duì)象將不能被 JVM 正车谆回收,導(dǎo)致內(nèi)存泄露锚沸。
可防止內(nèi)存泄漏示例如下:
public class AppManager {
private volatile static AppManager instance;
private Context context;
private AppManager(Context context) {
this.context = context;
}
public static AppManager getInstance(Context context) {
if (instance != null) {
synchronized (AppManager.class) {
if (instance == null) {
instance = new AppManager(context);
}
}
return instance;
}
}
在上例中跋选,不管傳入什么Context最終將使用Application的Context,而單例的生命周期和應(yīng)用的一樣長(zhǎng)哗蜈,這樣就防止了內(nèi)存泄漏前标。
-
非靜態(tài)內(nèi)部類創(chuàng)建靜態(tài)實(shí)例
有的時(shí)候我們可能會(huì)在啟動(dòng)頻繁的Activity中,為了避免重復(fù)創(chuàng)建相同的數(shù)據(jù)資源距潘,可能會(huì)出現(xiàn)這種寫法:
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(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)使用ApplicationContext陷猫。
-
Handler
Handler的使用造成的內(nèi)存泄漏問(wèn)題應(yīng)該說(shuō)最為常見(jiàn)了秫舌,平時(shí)在處理網(wǎng)絡(luò)任務(wù)或者封裝一些請(qǐng)求回調(diào)等api都應(yīng)該會(huì)借助Handler來(lái)處理,對(duì)于Handler的使用代碼編寫一不規(guī)范即有可能造成內(nèi)存泄漏绣檬,要知道足陨,只要 Handler 發(fā)送的 Message 尚未被處理,則該 Message 及發(fā)送它的 Handler 對(duì)象將被線程 MessageQueue 一直持有娇未。由于 Handler 屬于 TLS(Thread Local Storage) 變量, 生命周期和 Activity 是不一致的墨缘。因此這種實(shí)現(xiàn)方式一般很難保證跟 View 或者 Activity 的生命周期保持一致,故很容易導(dǎo)致無(wú)法正確釋放零抬。
示例如下:
public class MainActivity extends AppCompatActivity {
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
//...
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
loadData();
}
private void loadData(){
//...request
Message message = Message.obtain();
mHandler.sendMessage(message);
}
}
這種創(chuàng)建Handler的方式會(huì)造成內(nèi)存泄漏镊讼,由于mHandler是Handler的非靜態(tài)匿名內(nèi)部類的實(shí)例,所以它持有外部類Activity的強(qiáng)引用平夜,我們知道消息隊(duì)列是在一個(gè)Looper線程中不斷輪詢處理消息蝶棋,那么當(dāng)這個(gè)Activity退出時(shí)消息隊(duì)列中還有未處理的消息或者正在處理消息,而消息隊(duì)列中的Message持有mHandler實(shí)例的引用忽妒,mHandler又持有Activity的引用玩裙,所以導(dǎo)致該Activity的內(nèi)存資源無(wú)法及時(shí)回收,引發(fā)內(nèi)存泄漏段直∠仔铮可采用下面方式:
public class MainActivity extends AppCompatActivity {
private MyHandler mHandler = new MyHandler(this);
private TextView mTextView ;
private static class MyHandler extends Handler {
private WeakReference<context> reference;
public MyHandler(Context context) {
reference = new WeakReference<>(context);
}
@Override
public void handleMessage(Message msg) {
MainActivity activity = (MainActivity) reference.get();
if(activity != null){
activity.mTextView.setText("");
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextView = (TextView)findViewById(R.id.textview);
loadData();
}
private void loadData() {
//...request
Message message = Message.obtain();
mHandler.sendMessage(message);
}
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
}
}
創(chuàng)建一個(gè)靜態(tài)Handler內(nèi)部類,然后對(duì)Handler持有的對(duì)象使用弱引用坷牛,這樣在回收時(shí)也可以回收Handler持有的對(duì)象罕偎,這樣雖然避免了Activity泄漏,不過(guò)Looper線程的消息隊(duì)列中還是可能會(huì)有待處理的消息京闰,所以我們?cè)贏ctivity的Destroy時(shí)或者Stop時(shí)應(yīng)該移除消息隊(duì)列中的消息,使用mHandler.removeCallbacksAndMessages(null);是移除消息隊(duì)列中所有消息和所有的Runnable颜及。當(dāng)然也可以使用mHandler.removeCallbacks();或mHandler.removeMessages();來(lái)移除指定的Runnable和Message。
-
線程內(nèi)存泄露
線程也是造成內(nèi)存泄露的一個(gè)重要的源頭蹂楣。線程產(chǎn)生內(nèi)存泄露的主要原因在于線程生命周期的不可控俏站。比如線程是 Activity 的內(nèi)部類(匿名內(nèi)部類和非靜態(tài)內(nèi)部類持有外部類的強(qiáng)引用),則線程對(duì)象中保存了 Activity 的一個(gè)引用痊土,當(dāng)線程的 run 函數(shù)耗時(shí)較長(zhǎng)沒(méi)有結(jié)束時(shí)肄扎,線程對(duì)象是不會(huì)被銷毀的,因此它所引用的老的 Activity 內(nèi)存資源無(wú)法回收也不會(huì)被銷毀赁酝,因此就出現(xiàn)了內(nèi)存泄露的問(wèn)題犯祠。
正確的做法還是使用靜態(tài)內(nèi)部類的方式。 -
資源未關(guān)閉
對(duì)于使用了BraodcastReceiver酌呆,ContentObserver衡载,F(xiàn)ile,Cursor隙袁,Stream痰娱,Bitmap等資源的使用弃榨,應(yīng)該在Activity銷毀時(shí)及時(shí)關(guān)閉或者注銷,否則這些資源將不會(huì)被回收梨睁,造成內(nèi)存泄漏鲸睛。
總結(jié):
- 對(duì) Activity 等組件的引用應(yīng)該控制在 Activity 的生命周期之內(nèi); 如果不能控制就考慮使用 getApplicationContext 或者 getApplication坡贺,以避免 Activity 被外部比Activity生命周期長(zhǎng)的對(duì)象引用而泄露官辈。
- 盡量不要在靜態(tài)變量或者靜態(tài)內(nèi)部類中使用非靜態(tài)外部成員變量(包括context ),即使要使用拴念,也要考慮適當(dāng)時(shí)機(jī)把外部成員變量置空钧萍;也可以在內(nèi)部類中使用弱引用來(lái)引用外部類的變量來(lái)避免內(nèi)存泄漏褐缠。
- Handler 的持有的引用對(duì)象最好使用弱引用政鼠,資源釋放時(shí)也可以清空 Handler 里面的消息。比如在 Activity onStop 或者 onDestroy 的時(shí)候队魏,取消掉該 Handler 對(duì)象的 Message和 Runnable.
- 保持對(duì)對(duì)象生命周期的敏感公般,特別注意單例、靜態(tài)對(duì)象胡桨、全局性集合等的生命周期官帘。
- 線程 Runnable 執(zhí)行耗時(shí)操作,注意在頁(yè)面返回時(shí)及時(shí)取消或者把 Runnable 寫成靜態(tài)類昧谊。
a) 如果線程類是內(nèi)部類刽虹,改為靜態(tài)內(nèi)部類。
b) 線程內(nèi)如果需要引用外部類對(duì)象如 context呢诬,需要使用弱引用涌哲。 - 在 Java 的實(shí)現(xiàn)過(guò)程中,也要考慮其對(duì)象釋放尚镰,最好的方法是在不使用某對(duì)象時(shí)阀圾,顯式地將此對(duì)象賦空,如清空對(duì)圖片等資源有直接引用或者間接引用的數(shù)組(使用 array.clear() ; array = null)狗唉,最好遵循誰(shuí)創(chuàng)建誰(shuí)釋放的原則初烘。