內存泄露從入門到精通三部曲之基礎知識篇

騰訊Bugly特約做者: 姚潮生java

首先以一個內存泄露實例來開始本節基礎概念的內容。程序員

實例1:單例致使內存對象沒法釋放而泄露

能夠看出ImageUtil這個工具類是一個單例,並引用了activity的context。算法

試想這個場景,應用起來之後,轉屏。轉屏之後,舊MainActivity會destroy,新MainActivity會重建,致使單例ImageUtil從新getInstance。很不幸的是,因爲instance已經不是空的了,因此ImageUtil不會重建,還持有以前的Context,也就是以前的那個MainActivity實例的context,所以會形成兩個問題:編程

功能問題:使用ImageUitl訪問context相關內容時可能會發生異常(由於當前context並非當前activity的context);windows

內存泄露:舊context被生命週期更長的靜態變量持有而致使activity沒法釋放形成泄漏!(所以靜態變量是很容易所以內存泄露的!)數組

使用工具能夠看到ImageUtil引用了MainActivity致使MainActivity駐留內存發生泄漏。緩存

備註:本系列部分概念和例子引用來自網絡。網絡

內存泄露,咱們要研究的泄露對象究竟是什麼?

首先咱們來了解程序運行時,所需內存的分配策略:函數

按照編譯原理的觀點,程序運行時的內存分配有三種策略,分別是靜態的、棧式的、和堆式的,對應的,三種存儲策略使用的內存空間主要分別是靜態存儲區(也稱方法區)、堆區和棧區。他們的功能不一樣,對他們使用方式也就不一樣。工具

  • 靜態存儲區(方法區):內存在程序編譯的時候就已經分配好,這塊內存在程序整個運行期間都存在。它主要存放靜態數據、全局static數據和常量。

  • 棧區:在執行函數時,函數內局部變量的存儲單元均可以在棧上建立,函數執行結束時這些存儲單元自動被釋放。棧內存分配運算內置於處理器的指令集中,效率很高,可是分配的內存容量有限。

  • 堆區:亦稱動態內存分配。程序在運行的時候用malloc或new申請任意大小的內存,程序員本身負責在適當的時候用free或delete釋放內存(Java則依賴垃圾回收器)。動態內存的生存期能夠由咱們決定,若是咱們不釋放內存,程序將在最後才釋放掉動態內存。 可是,良好的編程習慣是:若是某動態內存再也不使用,須要將其釋放掉。

接下來咱們集中說下堆和棧的區別:

在函數中(說明是局部變量)定義的一些基本類型的變量和對象的引用變量都是在函數的棧內存中分配。當在一段代碼塊中定義一個變量時,java就在棧中爲這個變量分配內存空間,當超過變量的做用域後,java會自動釋放掉爲該變量分配的內存空間,該內存空間能夠馬上被另做他用。

堆內存用於存放全部由new建立的對象(內容包括該對象其中的全部成員變量)和數組。在堆中分配的內存,由java虛擬機自動垃圾回收器來管理。在堆中產生了一個數組或者對象後,還能夠在棧中定義一個特殊的變量,這個變量的取值等於數組或者對象在堆內存中的首地址,在棧中的這個特殊的變量就變成了數組或者對象的引用變量,之後就能夠在程序中使用棧內存中的引用變量來訪問堆中的數組或者對象,引用變量至關於爲數組或者對象起的一個別名,或者代號。

堆是不連續的內存區域(由於系統是用鏈表來存儲空閒內存地址,天然不是連續的),堆大小受限於計算機系統中有效的虛擬內存(32bit系統理論上是4G),因此堆的空間比較靈活,比較大。棧是一塊連續的內存區域,大小是操做系統預約好的,windows下棧大小是2M(也有是1M,在編譯時肯定,VC中可設置)。

對於堆,頻繁的new/delete會形成大量內存碎片,使程序效率下降。對於棧,它是先進後出的隊列,進出一一對應,不產生碎片,運行效率穩定高。

舉一個關於變量存儲位置的實例2:

結論

局部變量的基本數據類型和引用存儲於棧中,引用的對象實體存儲於堆中。——由於它們屬於方法中的變量,生命週期隨方法而結束。

成員變量所有存儲與堆中(包括基本數據類型,引用和引用的對象實體)——由於它們屬於類,類對象終究是要被new出來使用的。

回到咱們的問題:內存泄露須要關注的是什麼?

咱們這裏說的內存泄露,是針對,也只針對堆內存,他們存放的就是引用指向的對象實體。

那麼第二個問題就是,內存爲何會泄露?

爲了判斷Java中是否有內存泄露,咱們首先必須瞭解Java是如何管理(堆)內存的。Java的內存管理就是對象的分配和釋放問題。在Java中,內存的分配是由程序完成的,而內存的釋放是由垃圾收集器(Garbage Collection,GC)完成的,程序員不須要經過調用函數來釋放內存,但它只能回收無用而且再也不被其它對象引用的那些對象所佔用的空間。

Java的內存垃圾回收機制是從程序的主要運行對象(如靜態對象/寄存器/棧上指向的堆內存對象等)開始檢查引用鏈,當遍歷一遍後獲得上述這些沒法回收的對象和他們所引用的對象鏈,組成沒法回收的對象集合,而其餘孤立對象(集)就做爲垃圾回收。GC爲了可以正確釋放對象,必須監控每個對象的運行狀態,包括對象的申請、引用、被引用、賦值等,GC都須要進行監控。監視對象狀態是爲了更加準確地、及時地釋放對象,而釋放對象的根本原則就是該對象再也不被引用。

在Java中,這些無用的對象都由GC負責回收,所以程序員不須要考慮這部分的內存泄露。雖然,咱們有幾個函數能夠訪問GC,例如運行GC的函數System.gc(),可是根據Java語言規範定義,該函數不保證JVM的垃圾收集器必定會執行。由於不一樣的JVM實現者可能使用不一樣的算法管理GC。一般GC的線程的優先級別較低。JVM調用GC的策略也有不少種,有的是內存使用到達必定程度時,GC纔開始工做,也有定時執行的,有的是平緩執行GC,有的是中斷式執行GC。但一般來講,咱們不須要關心這些。

至此,咱們來看看Java中須要被回收的垃圾:

{
Person p1 = new Person();
……
}

引用句柄p1的做用域是從定義到「}」處,執行完這對大括號中的全部代碼後,產生的Person對象就會變成垃圾,由於引用這個對象的句柄p1已超過其做用域,p1失效,在棧中被銷燬,所以堆上的Person對象再也不被任何句柄引用了。 所以person變爲垃圾,會被回收。

從上面的例子和解釋,能夠看到一個很關鍵的詞:引用。

通俗的講,經過A能調用並訪問到B,那就說明A持有 B 的引用,或A就是B的引用,B的引用計數 +1。

  1. 好比 Person p1 = new Person();經過P1能操做Person對象,所以P1是Person的引用;

  2. 好比類O中有一個成員變量是I類對象,所以咱們可使用o.i的方式來訪問I類對象的成員,所以o持有一個i對象的引用。

GC過程與對象的引用類型是嚴重相關的,咱們來看看Java對引用的分類Strong reference, SoftReference, WeakReference, PhatomReference

講多一步,這裏的軟引用/弱引用通常是作什麼的呢?

在Android應用的開發中,爲了防止內存溢出,在處理一些佔用內存大並且聲明週期較長的對象時候,能夠儘可能應用軟引用和弱引用技術。

軟/弱引用能夠和一個引用隊列(ReferenceQueue)聯合使用,若是軟引用所引用的對象被垃圾回收器回收,Java虛擬機就會把這個軟引用加入到與之關聯的引用隊列中。利用這個隊列能夠得知被回收的軟/弱引用的對象列表,從而爲緩衝器清除已失效的軟/弱引用。

假設咱們的應用會用到大量的默認圖片,好比應用中有默認的頭像,默認遊戲圖標等等,這些圖片不少地方會用到。若是每次都去讀取圖片,因爲讀取文件須要硬件操做,速度較慢,會致使性能較低。因此咱們考慮將圖片緩存起來,須要的時候直接從內存中讀取。可是,因爲圖片佔用內存空間比較大,緩存不少圖片須要不少的內存,就可能比較容易發生OutOfMemory異常。這時,咱們能夠考慮使用軟/弱引用技術來避免這個問題發生。如下就是高速緩衝器的雛形:

首先定義一個HashMap,保存軟引用對象。

private Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>();

再來定義一個方法,保存Bitmap的軟引用到HashMap。

public class CacheBySoftRef {
    // 首先定義一個HashMap,保存軟引用對象。
    private Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>();
    // 再來定義一個方法,保存Bitmap的軟引用到HashMap。
    public void addBitmapToCache(String path) {
        // 強引用的Bitmap對象
        Bitmap bitmap = BitmapFactory.decodeFile(path);
        // 軟引用的Bitmap對象
        SoftReference<Bitmap> softBitmap = new SoftReference<Bitmap>(bitmap);
        // 添加該對象到Map中使其緩存
        imageCache.put(path, softBitmap);
    }
    // 獲取的時候,能夠經過SoftReference的get()方法獲得Bitmap對象。
    public Bitmap getBitmapByPath(String path) {
        // 從緩存中取軟引用的Bitmap對象
        SoftReference<Bitmap> softBitmap = imageCache.get(path);
        // 判斷是否存在軟引用
        if (softBitmap == null) {
            return null;
        }
        // 經過軟引用取出Bitmap對象,若是因爲內存不足Bitmap被回收,將取得空 ,若是未被回收,則可重複使用,提升速度。
        Bitmap bitmap = softBitmap.get();
        return bitmap;
    }
}

使用軟引用之後,在OutOfMemory異常發生以前,這些緩存的圖片資源的內存空間能夠被釋放掉的,從而避免內存達到上限,避免Crash發生。

若是隻是想避免OutOfMemory異常的發生,則可使用軟引用。若是對於應用的性能更在乎,想盡快回收一些佔用內存比較大的對象,則可使用弱引用。

另外能夠根據對象是否常用來判斷選擇軟引用仍是弱引用。若是該對象可能會常用的,就儘可能用軟引用。若是該對象不被使用的可能性更大些,就能夠用弱引用。

回到咱們的問題,爲何內存會泄露?

堆內存中的長生命週期的對象持有短生命週期對象的強/軟引用,儘管短生命週期對象已經再也不須要,可是由於長生命週期對象持有它的引用而致使不能被回收,這就是Java中內存泄露的根本緣由。


Bugly 是騰訊內部產品質量監控平臺的外發版本,其主要功能是App發佈之後,對用戶側發生的Crash以及卡頓現象進行監控並上報,讓開發同窗能夠第一時間瞭解到App的質量狀況,及時機型修改。目前騰訊內部全部的產品,均在使用其進行線上產品的崩潰監控。

相關文章
相關標籤/搜索