摘要:騰訊特約作者姚潮生首先以一個內(nèi)存泄露實例來開始本節(jié)基礎概念的內(nèi)容。堆內(nèi)存用于存放所有由創(chuàng)建的對象內(nèi)容包括該對象其中的所有成員變量和數(shù)組?;氐轿覀兊膯栴},為什么內(nèi)存會泄露堆內(nèi)存中的長生命周期的對象持有短生命周期對象的強軟引用,盡管
騰訊Bugly特約作者: 姚潮生
首先以一個內(nèi)存泄露實例來開始本節(jié)基礎概念的內(nèi)容。
實例1:單例導致內(nèi)存對象無法釋放而泄露可以看出ImageUtil這個工具類是一個單例,并引用了activity的context。
試想這個場景,應用起來以后,轉(zhuǎn)屏。轉(zhuǎn)屏以后,舊MainActivity會destroy,新MainActivity會重建,導致單例ImageUtil重新getInstance。很不幸的是,由于instance已經(jīng)不是空的了,所以ImageUtil不會重建,還持有之前的Context,也就是之前的那個MainActivity實例的context,因此會造成兩個問題:
功能問題:使用ImageUitl訪問context相關內(nèi)容時可能會發(fā)生異常(因為當前context并不是當前activity的context);
內(nèi)存泄露:舊context被生命周期更長的靜態(tài)變量持有而導致activity無法釋放造成泄漏?。ㄒ虼遂o態(tài)變量是很容易因此內(nèi)存泄露的?。?/p>
使用工具可以看到ImageUtil引用了MainActivity導致MainActivity駐留內(nèi)存發(fā)生泄漏。
備注:本系列部分概念和例子引用來自網(wǎng)絡。
內(nèi)存泄露,我們要研究的泄露對象到底是什么?首先我們來了解程序運行時,所需內(nèi)存的分配策略:
按照編譯原理的觀點,程序運行時的內(nèi)存分配有三種策略,分別是靜態(tài)的、棧式的、和堆式的,對應的,三種存儲策略使用的內(nèi)存空間主要分別是靜態(tài)存儲區(qū)(也稱方法區(qū))、堆區(qū)和棧區(qū)。他們的功能不同,對他們使用方式也就不同。
靜態(tài)存儲區(qū)(方法區(qū)):內(nèi)存在程序編譯的時候就已經(jīng)分配好,這塊內(nèi)存在程序整個運行期間都存在。它主要存放靜態(tài)數(shù)據(jù)、全局static數(shù)據(jù)和常量。
棧區(qū):在執(zhí)行函數(shù)時,函數(shù)內(nèi)局部變量的存儲單元都可以在棧上創(chuàng)建,函數(shù)執(zhí)行結(jié)束時這些存儲單元自動被釋放。棧內(nèi)存分配運算內(nèi)置于處理器的指令集中,效率很高,但是分配的內(nèi)存容量有限。
堆區(qū):亦稱動態(tài)內(nèi)存分配。程序在運行的時候用malloc或new申請任意大小的內(nèi)存,程序員自己負責在適當?shù)臅r候用free或delete釋放內(nèi)存(Java則依賴垃圾回收器)。動態(tài)內(nèi)存的生存期可以由我們決定,如果我們不釋放內(nèi)存,程序?qū)⒃谧詈蟛裴尫诺魟討B(tài)內(nèi)存。 但是,良好的編程習慣是:如果某動態(tài)內(nèi)存不再使用,需要將其釋放掉。
接下來我們集中說下堆和棧的區(qū)別:
在函數(shù)中(說明是局部變量)定義的一些基本類型的變量和對象的引用變量都是在函數(shù)的棧內(nèi)存中分配。當在一段代碼塊中定義一個變量時,java就在棧中為這個變量分配內(nèi)存空間,當超過變量的作用域后,java會自動釋放掉為該變量分配的內(nèi)存空間,該內(nèi)存空間可以立刻被另作他用。
堆內(nèi)存用于存放所有由new創(chuàng)建的對象(內(nèi)容包括該對象其中的所有成員變量)和數(shù)組。在堆中分配的內(nèi)存,由java虛擬機自動垃圾回收器來管理。在堆中產(chǎn)生了一個數(shù)組或者對象后,還可以在棧中定義一個特殊的變量,這個變量的取值等于數(shù)組或者對象在堆內(nèi)存中的首地址,在棧中的這個特殊的變量就變成了數(shù)組或者對象的引用變量,以后就可以在程序中使用棧內(nèi)存中的引用變量來訪問堆中的數(shù)組或者對象,引用變量相當于為數(shù)組或者對象起的一個別名,或者代號。
堆是不連續(xù)的內(nèi)存區(qū)域(因為系統(tǒng)是用鏈表來存儲空閑內(nèi)存地址,自然不是連續(xù)的),堆大小受限于計算機系統(tǒng)中有效的虛擬內(nèi)存(32bit系統(tǒng)理論上是4G),所以堆的空間比較靈活,比較大。棧是一塊連續(xù)的內(nèi)存區(qū)域,大小是操作系統(tǒng)預定好的,windows下棧大小是2M(也有是1M,在編譯時確定,VC中可設置)。
對于堆,頻繁的new/delete會造成大量內(nèi)存碎片,使程序效率降低。對于棧,它是先進后出的隊列,進出一一對應,不產(chǎn)生碎片,運行效率穩(wěn)定高。
舉一個關于變量存儲位置的實例2:
結(jié)論:
局部變量的基本數(shù)據(jù)類型和引用存儲于棧中,引用的對象實體存儲于堆中?!驗樗鼈儗儆诜椒ㄖ械淖兞?,生命周期隨方法而結(jié)束。
成員變量全部存儲與堆中(包括基本數(shù)據(jù)類型,引用和引用的對象實體)——因為它們屬于類,類對象終究是要被new出來使用的。
回到我們的問題:內(nèi)存泄露需要關注的是什么?
我們這里說的內(nèi)存泄露,是針對,也只針對堆內(nèi)存,他們存放的就是引用指向的對象實體。
那么第二個問題就是,內(nèi)存為什么會泄露?為了判斷Java中是否有內(nèi)存泄露,我們首先必須了解Java是如何管理(堆)內(nèi)存的。Java的內(nèi)存管理就是對象的分配和釋放問題。在Java中,內(nèi)存的分配是由程序完成的,而內(nèi)存的釋放是由垃圾收集器(Garbage Collection,GC)完成的,程序員不需要通過調(diào)用函數(shù)來釋放內(nèi)存,但它只能回收無用并且不再被其它對象引用的那些對象所占用的空間。
Java的內(nèi)存垃圾回收機制是從程序的主要運行對象(如靜態(tài)對象/寄存器/棧上指向的堆內(nèi)存對象等)開始檢查引用鏈,當遍歷一遍后得到上述這些無法回收的對象和他們所引用的對象鏈,組成無法回收的對象集合,而其他孤立對象(集)就作為垃圾回收。GC為了能夠正確釋放對象,必須監(jiān)控每一個對象的運行狀態(tài),包括對象的申請、引用、被引用、賦值等,GC都需要進行監(jiān)控。監(jiān)視對象狀態(tài)是為了更加準確地、及時地釋放對象,而釋放對象的根本原則就是該對象不再被引用。
在Java中,這些無用的對象都由GC負責回收,因此程序員不需要考慮這部分的內(nèi)存泄露。雖然,我們有幾個函數(shù)可以訪問GC,例如運行GC的函數(shù)System.gc(),但是根據(jù)Java語言規(guī)范定義,該函數(shù)不保證JVM的垃圾收集器一定會執(zhí)行。因為不同的JVM實現(xiàn)者可能使用不同的算法管理GC。通常GC的線程的優(yōu)先級別較低。JVM調(diào)用GC的策略也有很多種,有的是內(nèi)存使用到達一定程度時,GC才開始工作,也有定時執(zhí)行的,有的是平緩執(zhí)行GC,有的是中斷式執(zhí)行GC。但通常來說,我們不需要關心這些。
至此,我們來看看Java中需要被回收的垃圾:
{ Person p1 = new Person(); …… }
引用句柄p1的作用域是從定義到“}”處,執(zhí)行完這對大括號中的所有代碼后,產(chǎn)生的Person對象就會變成垃圾,因為引用這個對象的句柄p1已超過其作用域,p1失效,在棧中被銷毀,因此堆上的Person對象不再被任何句柄引用了。 因此person變?yōu)槔?,會被回收?/p>
從上面的例子和解釋,可以看到一個很關鍵的詞:引用。
通俗的講,通過A能調(diào)用并訪問到B,那就說明A持有 B 的引用,或A就是B的引用,B的引用計數(shù) +1。
比如 Person p1 = new Person();通過P1能操作Person對象,因此P1是Person的引用;
比如類O中有一個成員變量是I類對象,因此我們可以使用o.i的方式來訪問I類對象的成員,因此o持有一個i對象的引用。
GC過程與對象的引用類型是嚴重相關的,我們來看看Java對引用的分類Strong reference, SoftReference, WeakReference, PhatomReference
講多一步,這里的軟引用/弱引用一般是做什么的呢?
在Android應用的開發(fā)中,為了防止內(nèi)存溢出,在處理一些占用內(nèi)存大而且聲明周期較長的對象時候,可以盡量應用軟引用和弱引用技術(shù)。
軟/弱引用可以和一個引用隊列(ReferenceQueue)聯(lián)合使用,如果軟引用所引用的對象被垃圾回收器回收,Java虛擬機就會把這個軟引用加入到與之關聯(lián)的引用隊列中。利用這個隊列可以得知被回收的軟/弱引用的對象列表,從而為緩沖器清除已失效的軟/弱引用。
假設我們的應用會用到大量的默認圖片,比如應用中有默認的頭像,默認游戲圖標等等,這些圖片很多地方會用到。如果每次都去讀取圖片,由于讀取文件需要硬件操作,速度較慢,會導致性能較低。所以我們考慮將圖片緩存起來,需要的時候直接從內(nèi)存中讀取。但是,由于圖片占用內(nèi)存空間比較大,緩存很多圖片需要很多的內(nèi)存,就可能比較容易發(fā)生OutOfMemory異常。這時,我們可以考慮使用軟/弱引用技術(shù)來避免這個問題發(fā)生。以下就是高速緩沖器的雛形:
首先定義一個HashMap,保存軟引用對象。
private Map> imageCache = new HashMap >();
再來定義一個方法,保存Bitmap的軟引用到HashMap。
public class CacheBySoftRef { // 首先定義一個HashMap,保存軟引用對象。 private Map> imageCache = new HashMap >(); // 再來定義一個方法,保存Bitmap的軟引用到HashMap。 public void addBitmapToCache(String path) { // 強引用的Bitmap對象 Bitmap bitmap = BitmapFactory.decodeFile(path); // 軟引用的Bitmap對象 SoftReference softBitmap = new SoftReference (bitmap); // 添加該對象到Map中使其緩存 imageCache.put(path, softBitmap); } // 獲取的時候,可以通過SoftReference的get()方法得到Bitmap對象。 public Bitmap getBitmapByPath(String path) { // 從緩存中取軟引用的Bitmap對象 SoftReference softBitmap = imageCache.get(path); // 判斷是否存在軟引用 if (softBitmap == null) { return null; } // 通過軟引用取出Bitmap對象,如果由于內(nèi)存不足Bitmap被回收,將取得空 ,如果未被回收,則可重復使用,提高速度。 Bitmap bitmap = softBitmap.get(); return bitmap; } }
使用軟引用以后,在OutOfMemory異常發(fā)生之前,這些緩存的圖片資源的內(nèi)存空間可以被釋放掉的,從而避免內(nèi)存達到上限,避免Crash發(fā)生。
如果只是想避免OutOfMemory異常的發(fā)生,則可以使用軟引用。如果對于應用的性能更在意,想盡快回收一些占用內(nèi)存比較大的對象,則可以使用弱引用。
另外可以根據(jù)對象是否經(jīng)常使用來判斷選擇軟引用還是弱引用。如果該對象可能會經(jīng)常使用的,就盡量用軟引用。如果該對象不被使用的可能性更大些,就可以用弱引用。
回到我們的問題,為什么內(nèi)存會泄露?
堆內(nèi)存中的長生命周期的對象持有短生命周期對象的強/軟引用,盡管短生命周期對象已經(jīng)不再需要,但是因為長生命周期對象持有它的引用而導致不能被回收,這就是Java中內(nèi)存泄露的根本原因。
Bugly 是騰訊內(nèi)部產(chǎn)品質(zhì)量監(jiān)控平臺的外發(fā)版本,其主要功能是App發(fā)布以后,對用戶側(cè)發(fā)生的Crash以及卡頓現(xiàn)象進行監(jiān)控并上報,讓開發(fā)同學可以第一時間了解到App的質(zhì)量情況,及時機型修改。目前騰訊內(nèi)部所有的產(chǎn)品,均在使用其進行線上產(chǎn)品的崩潰監(jiān)控。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/64733.html
摘要:對比操作前后的來定位內(nèi)存泄露的根因所在。手機管家內(nèi)存泄露每日監(jiān)控方案目前手機管家的內(nèi)存泄露每日監(jiān)控會自動運行并輸出是否存在疑似泄露的報告郵件,不論泄露對象的大小。 騰訊Bugly特約作者: 姚潮生 最原始的內(nèi)存泄露測試 重復多次操作關鍵的可疑的路徑,從內(nèi)存監(jiān)控工具中觀察內(nèi)存曲線,是否存在不斷上升的趨勢且不會在程序返回時明顯回落。這種方式可以發(fā)現(xiàn)最基本,也是最明顯的內(nèi)存泄露問題,對用戶價...
摘要:對比操作前后的來定位內(nèi)存泄露的根因所在。手機管家內(nèi)存泄露每日監(jiān)控方案目前手機管家的內(nèi)存泄露每日監(jiān)控會自動運行并輸出是否存在疑似泄露的報告郵件,不論泄露對象的大小。 騰訊Bugly特約作者: 姚潮生 最原始的內(nèi)存泄露測試 重復多次操作關鍵的可疑的路徑,從內(nèi)存監(jiān)控工具中觀察內(nèi)存曲線,是否存在不斷上升的趨勢且不會在程序返回時明顯回落。這種方式可以發(fā)現(xiàn)最基本,也是最明顯的內(nèi)存泄露問題,對用戶價...
閱讀 2022·2021-09-30 09:53
閱讀 1863·2021-09-24 09:48
閱讀 1769·2019-08-30 14:01
閱讀 2183·2019-08-29 18:35
閱讀 1260·2019-08-26 18:27
閱讀 2996·2019-08-26 12:12
閱讀 963·2019-08-23 17:16
閱讀 958·2019-08-23 15:31