Android内存泄露总结
內(nèi)存泄露是如何產(chǎn)生的?
當(dāng)一個(gè)對(duì)象已經(jīng)不需要再使用了,本該被回收時(shí),而有另外一個(gè)正在使用的對(duì)象持有它的引用從而導(dǎo)致它不能被回收,這導(dǎo)致本該被回收的對(duì)象不能被回收而停留在堆內(nèi)存中,這就產(chǎn)生了內(nèi)存泄漏。
內(nèi)存泄露是造成OOM的主要原因之一。
內(nèi)存泄露的對(duì)象是什么?
內(nèi)存分配有三種策略:靜態(tài)(靜態(tài)存儲(chǔ)區(qū)/方法區(qū))、棧、堆。
靜態(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)厥掌?#xff09;。動(dòng)態(tài)內(nèi)存的生存期可以由我們決定,如果我們不釋放內(nèi)存,程序?qū)⒃谧詈蟛裴尫诺魟?dòng)態(tài)內(nèi)存。
堆和棧的區(qū)別:
棧:定義一些基本數(shù)據(jù)類型變量和應(yīng)用變量。當(dāng)在一段代碼塊中定義一個(gè)變量時(shí),java就在棧中為這個(gè)變量分配內(nèi)存空間,當(dāng)超過變量的作用域后,java會(huì)自動(dòng)釋放掉為該變量分配的內(nèi)存空間,該內(nèi)存空間可以立刻被另作他用。
堆:用于存放所有由new創(chuàng)建的對(duì)象(內(nèi)容包括該對(duì)象其中的所有成員變量)和數(shù)組。在堆中分配的內(nèi)存,由java虛擬機(jī)自動(dòng)垃圾回收器來管理。在堆中產(chǎn)生了一個(gè)數(shù)組或者對(duì)象后,還可以在棧中定義一個(gè)特殊的變量,這個(gè)變量的取值等于數(shù)組或者對(duì)象在堆內(nèi)存中的首地址,在棧中的這個(gè)特殊的變量就變成了數(shù)組或者對(duì)象的引用變量,以后就可以在程序中使用棧內(nèi)存中的引用變量來訪問堆中的數(shù)組或者對(duì)象,引用變量相當(dāng)于為數(shù)組或者對(duì)象起的一個(gè)別名,或者代號(hào)。
堆是不連續(xù)的內(nèi)存區(qū)域(因?yàn)橄到y(tǒng)是用鏈表來存儲(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)樗鼈儗儆诜椒ㄖ械淖兞?#xff0c;生命周期隨方法而結(jié)束。
成員變量全部存儲(chǔ)與堆中(包括基本數(shù)據(jù)類型,引用和引用的對(duì)象實(shí)體)
——因?yàn)樗鼈儗儆陬?#xff0c;類對(duì)象終究是要被new出來使用的。
內(nèi)存為什么會(huì)泄露?
Java的內(nèi)存管理就是對(duì)象的分配和釋放問題。在Java中,內(nèi)存的分配是由程序完成的,而內(nèi)存的釋放是由垃圾收集器(Garbage Collection,GC)完成的,程序員不需要通過調(diào)用函數(shù)來釋放內(nèi)存,但它只能回收無用并且不再被其它對(duì)象引用的那些對(duì)象所占用的空間。
堆內(nèi)存中的長(zhǎng)生命周期的對(duì)象持有短生命周期對(duì)象的強(qiáng)/軟引用,盡管短生命周期對(duì)象已經(jīng)不再需要,但是因?yàn)殚L(zhǎng)生命周期對(duì)象持有它的引用而導(dǎo)致不能被回收,這就是Java中內(nèi)存泄露的根本原因。
Java內(nèi)存回收機(jī)制:
從程序的主要運(yùn)行對(duì)象(如靜態(tài)對(duì)象/寄存器/棧上指向的堆內(nèi)存對(duì)象等)開始檢查引用鏈,當(dāng)遍歷一遍后得到上述這些無法回收的對(duì)象和他們所引用的對(duì)象鏈,組成無法回收的對(duì)象集合,而其他孤立對(duì)象(集)就作為垃圾回收。GC為了能夠正確釋放對(duì)象,必須監(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ì)象不再被引用。
關(guān)于“引用”:通俗的講,通過A能調(diào)用并訪問到B,那就說明A持有B的引用,或A就是B的引用。
Java中對(duì)引用的分類:
在Android應(yīng)用的開發(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ì)象列表,從而為緩沖器清除已失效的軟/弱引用。
常見的內(nèi)存泄露情況:
1、集合類泄露
集合類如果僅僅有添加元素的方法,而沒有相應(yīng)的刪除機(jī)制,導(dǎo)致內(nèi)存被占用。如果這個(gè)集合類是全局性的變量 (比如類中的靜態(tài)屬性,全局性的 map 等即有靜態(tài)引用或 final 一直指向它),那么沒有相應(yīng)的刪除機(jī)制,很可能導(dǎo)致集合所占用的內(nèi)存只增不減。
2、非靜態(tài)內(nèi)部類創(chuàng)建靜態(tài)實(shí)例造成內(nèi)存泄漏。
修復(fù)方法:將該內(nèi)部類設(shè)為靜態(tài)內(nèi)部類或?qū)⒃搩?nèi)部類抽取出來封裝成一個(gè)單例,如果需要使用Context,推薦的使用Application 的 Context。
匿名內(nèi)部類有可能會(huì)持有當(dāng)前Activity實(shí)例,如果將這個(gè)引用傳入到異步線程,此線程和Activity生命周期不一致是,將會(huì)造成內(nèi)存泄露。
3、Handler 造成的內(nèi)存泄漏
Handler 發(fā)送的 Message 尚未被處理,則該 Message 及發(fā)送它的 Handler 對(duì)象將被線程 MessageQueue 一直持有,且Handler的聲明周期和Activity的不一致,容易導(dǎo)致內(nèi)存無法被正確釋放。
修復(fù)方法:在Activity中避免使用非靜態(tài)內(nèi)部類,即將Handler改為static,此時(shí)Handler的存活周期將于Activity無關(guān),同事以弱應(yīng)用的方式引入Activity,避免直接將Activity作為context傳入。每次使用前需判空?!?/p>
Looper 線程的消息隊(duì)列中還是可能會(huì)有待處理的消息,所以我們?cè)?Activity 的 Destroy 時(shí)或者 Stop 時(shí)應(yīng)該移除消息隊(duì)列 MessageQueue 中的消息。
public class MainActivity extends AppCompatActivity {private MyHandler mHandler = new MyHandler(this);private TextView mTextView ;private static class MyHandler extends Handler {private WeakReference reference;public MyHandler(Context context) {reference = new WeakReference<>(context);}@Overridepublic void handleMessage(Message msg) {MainActivity activity = (MainActivity) reference.get();if(activity != null){activity.mTextView.setText("");}}}@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);mTextView = (TextView)findViewById(R.id.textview);loadData();}private void loadData() {//...requestMessage message = Message.obtain();mHandler.sendMessage(message);}@Overrideprotected void onDestroy() {super.onDestroy();mHandler.removeCallbacksAndMessages(null);} }4、盡量避免使用 static 成員變量
如果成員變量被聲明為 static,那我們都知道其生命周期將與整個(gè)app進(jìn)程生命周期一樣。將會(huì)導(dǎo)致app常駐內(nèi)存,從而導(dǎo)致app頻繁重啟,造成耗電、耗流量。
修復(fù)方法:不要在類初始時(shí)初始化靜態(tài)成員??梢钥紤]lazy初始化。
5、避免 override finalize()
finalize 方法被執(zhí)行的時(shí)間不確定,finalize 方法只會(huì)被執(zhí)行一次,即使對(duì)象被復(fù)活,如果已經(jīng)執(zhí)行過了 finalize 方法,再次被 GC 時(shí)也不會(huì)再執(zhí)行了,含有Finalize方法的object需要至少經(jīng)過兩輪GC才有可能被釋放。
6、線程泄露
線程也是造成內(nèi)存泄露的一個(gè)重要的源頭。線程產(chǎn)生內(nèi)存泄露的主要原因在于線程生命周期的不可控。比如線程是 Activity 的內(nèi)部類,則線程對(duì)象中保存了 Activity 的一個(gè)引用,當(dāng)線程的 run 函數(shù)耗時(shí)較長(zhǎng)沒有結(jié)束時(shí),線程對(duì)象是不會(huì)被銷毀的,因此它所引用的老的 Activity 也不會(huì)被銷毀,因此就出現(xiàn)了內(nèi)存泄露的問題?! ?/p>
6、資源未關(guān)閉造成的內(nèi)存泄漏
應(yīng)在Activity被銷毀時(shí)及時(shí)關(guān)閉或注銷BraodcastReceiver,ContentObserver,File,游標(biāo) Cursor,Stream,Bitmap等資源。
7、在 Java 的實(shí)現(xiàn)過程中,也要考慮其對(duì)象釋放,最好的方法是在不使用某對(duì)象時(shí),顯式地將此對(duì)象賦值為 null,比如使用完Bitmap 后先調(diào)用 recycle(),再賦為null,清空對(duì)圖片等資源有直接引用或者間接引用的數(shù)組(使用 array.clear() ; array = null)等,最好遵循誰創(chuàng)建誰釋放的原則。
8、單例造成的內(nèi)存泄露
由于單例的靜態(tài)特性使得單例的生命周期和應(yīng)用的生命周期一樣長(zhǎng),如果一個(gè)對(duì)象已經(jīng)不需要使用了,而單例對(duì)象還持有該對(duì)象的引用,則導(dǎo)致這個(gè)對(duì)象不能被正常回收,造成內(nèi)存泄露。
解決辦法:
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)來了
...context = getApplicationContext();... /** * 獲取全局的context * @return 返回全局context對(duì)象 */ public static Context getContext(){return context; }public class AppManager {private static AppManager instance;private Context context;private AppManager() {this.context = MyApplication.getContext();// 使用Application 的context }public static AppManager getInstance() {if (instance != null) {instance = new AppManager();}return instance;} }?
檢測(cè)程序中內(nèi)存泄露的工具:LeakCanary、MAT等
一些建議
1、對(duì)于生命周期比Activity長(zhǎng)的對(duì)象如果需要應(yīng)該使用ApplicationContext
2、對(duì)于需要在靜態(tài)內(nèi)部類中使用非靜態(tài)外部成員變量(如:Context、View ),可以在靜態(tài)內(nèi)部類中使用弱引用來引用外部類的變量來避免內(nèi)存泄漏
3、對(duì)于不再需要使用的對(duì)象,顯示的將其賦值為null,比如使用完Bitmap后先調(diào)用recycle(),再賦為null
4、保持對(duì)對(duì)象生命周期的敏感,特別注意單例、靜態(tài)對(duì)象、全局性集合等的生命周期
5、對(duì)于生命周期比Activity長(zhǎng)的內(nèi)部類對(duì)象,并且內(nèi)部類中使用了外部類的成員變量,可以這樣做避免內(nèi)存泄漏:
(1)將內(nèi)部類改為靜態(tài)內(nèi)部類
(2)靜態(tài)內(nèi)部類中使用弱引用來引用外部類的成員變量
6、在涉及到Context時(shí)先考慮ApplicationContext,當(dāng)然它并不是萬能的,對(duì)于有些地方則必須使用Activity的Context,對(duì)于Application,Service,Activity三者的Context的應(yīng)用場(chǎng)景如下:
?
參考文獻(xiàn):
http://dev.qq.com/topic/59152c9029d8be2a14b64dae
轉(zhuǎn)載于:https://www.cnblogs.com/yl-saber/p/7194394.html
總結(jié)
以上是生活随笔為你收集整理的Android内存泄露总结的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Vuejs自定义全局组件--loadin
- 下一篇: win7文件怎么加密码 如何给Win7文