Android 性能優(yōu)化之內(nèi)存泄漏檢測(cè)以及內(nèi)存優(yōu)化(上)

在 Java 中东羹,內(nèi)存的分配是由程序完成的剂桥,而內(nèi)存的釋放則是由 Garbage Collecation(GC) 完成的,Java/Android 程序員不用像 C/C++ 程序員一樣手動(dòng)調(diào)用相關(guān)函數(shù)來管理內(nèi)存的分配和釋放属提,雖然方便了很多权逗,但是這也就造成了內(nèi)存泄漏的可能性,所以記錄一下針對(duì) Android 應(yīng)用的內(nèi)存泄漏的檢測(cè)冤议,處理和優(yōu)化的相關(guān)內(nèi)容斟薇,上篇主要會(huì)分析 Java/Android 的內(nèi)存分配以及 GC 的詳細(xì)分析,中篇會(huì)闡述 Android 內(nèi)存泄漏的檢測(cè)和內(nèi)存泄漏的常見產(chǎn)生情景恕酸,下篇會(huì)分析一下內(nèi)存優(yōu)化的內(nèi)容堪滨。
  上篇:Android 性能優(yōu)化之內(nèi)存泄漏檢測(cè)以及內(nèi)存優(yōu)化(上)
  中篇:Android 性能優(yōu)化之內(nèi)存泄漏檢測(cè)以及內(nèi)存優(yōu)化(中)蕊温。
  下篇:Android 性能優(yōu)化之內(nèi)存泄漏檢測(cè)以及內(nèi)存優(yōu)化(下)袱箱。
  轉(zhuǎn)載請(qǐng)注明出處:http://blog.csdn.net/self_study/article/details/61919483
  對(duì)技術(shù)感興趣的同鞋加群544645972一起交流。

Java/Android 內(nèi)存分配和回收策略分析

這里需要提到的一點(diǎn)是在 Android 4.4 版本之前寿弱,使用的是和 Java 一樣的 Dalvik rumtime 機(jī)制犯眠,但是在 4.4 版本及以后,Android 引入了 ART 機(jī)制症革,ART 堆的分配與 GC 就和 Dalvik 的堆的分配與 GC 不一樣了筐咧,下面會(huì)介紹到(關(guān)于 Dalvik 和 ART 的對(duì)比:Android ART運(yùn)行時(shí)無縫替換Dalvik虛擬機(jī)的過程分析)。

Java/Android 內(nèi)存分配策略

Java/Android 程序運(yùn)行時(shí)的內(nèi)存分配有三種策略噪矛,分別是靜態(tài)的量蕊,棧式的和堆式的,對(duì)應(yīng)的三種存儲(chǔ)策略使用的內(nèi)存空間主要分別是靜態(tài)存儲(chǔ)區(qū)(方法區(qū))艇挨、堆區(qū)和棧區(qū):<ul><li>靜態(tài)存儲(chǔ)區(qū)(方法區(qū))</li>內(nèi)存在程序編譯的時(shí)候就已經(jīng)分配好残炮,這塊內(nèi)存在程序整個(gè)運(yùn)行期間都存在,它主要是用來存放靜態(tài)數(shù)據(jù)缩滨、全局 static 數(shù)據(jù)和常量势就;<li>棧區(qū)</li>在執(zhí)行函數(shù)時(shí)泉瞻,函數(shù)內(nèi)部局部變量的存儲(chǔ)單元都可以在棧上創(chuàng)建,函數(shù)執(zhí)行結(jié)束時(shí)這些存儲(chǔ)單元自動(dòng)被釋放苞冯,棧內(nèi)存分配運(yùn)算內(nèi)置于處理器的指令集中袖牙,效率很高,但是分配的內(nèi)存容量有限舅锄;<li>堆區(qū)</li>亦稱為動(dòng)態(tài)內(nèi)存分配鞭达,Java/Android 程序在適當(dāng)?shù)臅r(shí)候使用 new 關(guān)鍵字申請(qǐng)所需要大小的對(duì)象內(nèi)存,然后通過 GC 決定在不需要這塊對(duì)象內(nèi)存的時(shí)候回收它皇忿,但是由于我們的疏忽導(dǎo)致該對(duì)象在不需要繼續(xù)使用的之后畴蹭,GC 仍然沒辦法回收該內(nèi)存區(qū)域,這就代表發(fā)生了內(nèi)存泄漏鳍烁。</ul>  堆區(qū)和棧區(qū)的區(qū)別:
  在函數(shù)中定義的一些基本類型的變量和對(duì)象的引用變量(也就是局部變量的引用)都是在函數(shù)的棧內(nèi)存分配的叨襟,當(dāng)在一段代碼塊中定義一個(gè)變量時(shí),Java 就在棧中為這個(gè)變量分配內(nèi)存空間老翘,當(dāng)超過變量的作用域后芹啥,Java 會(huì)自動(dòng)釋放掉為該變量分配的內(nèi)存空間,該內(nèi)存空間可以立刻被重新使用铺峭;堆內(nèi)存用于存放所有由 new 創(chuàng)建的對(duì)象(內(nèi)容包括該對(duì)象其中的所有成員變量)和數(shù)組,在堆中分配的內(nèi)存是由 GC 來管理的汽纠,在堆中產(chǎn)生了一個(gè)對(duì)象或者數(shù)組后卫键,還可以在棧中生成一個(gè)引用指向這個(gè)堆中對(duì)象的內(nèi)存區(qū)域,以后就可以通過棧中這個(gè)引用變量來訪問堆中的這個(gè)引用指向的對(duì)象或者數(shù)組虱朵。下面這個(gè)圖片很好的說明了它兩的區(qū)別:
  

示例圖片

  堆是不連續(xù)的內(nèi)存區(qū)域(因?yàn)橄到y(tǒng)是用鏈表來存儲(chǔ)空閑內(nèi)存地址莉炉,所以隨著內(nèi)存的分配和釋放,肯定是不連續(xù)的)碴犬,堆的大小受限于計(jì)算機(jī)系統(tǒng)中有效的虛擬內(nèi)存(32bit 理論上是 4G)絮宁,所以堆的空間比較大,也比較靈活服协;棧是一塊連續(xù)的內(nèi)存區(qū)域绍昂,大小是操作系統(tǒng)預(yù)定好的,由于存儲(chǔ)的都是基本數(shù)據(jù)類型和對(duì)象的引用偿荷,所以大小一般不會(huì)太大窘游,在幾 M 左右。上面的這些差異導(dǎo)致頻繁的內(nèi)存申請(qǐng)和釋放造成堆內(nèi)存在大量的碎片跳纳,使得堆的運(yùn)行效率降低忍饰,而對(duì)于棧來說,它是先進(jìn)后出的隊(duì)列寺庄,不產(chǎn)生碎片艾蓝,運(yùn)行效率高力崇。
  綜上所述:<ul><li>局部變量的基本數(shù)據(jù)類型和引用存儲(chǔ)于棧中,引用的對(duì)象實(shí)體存儲(chǔ)于堆中赢织,因?yàn)樗鼈儗儆诜椒ㄖ械淖兞坎筒埽芷陔S方法而結(jié)束;</li><li>成員變量全部存儲(chǔ)于堆中(包括基本數(shù)據(jù)類型敌厘,對(duì)象引用和引用指向的對(duì)象實(shí)體)台猴,因?yàn)樗鼈儗儆陬悾悓?duì)象終究是要被 new 出來使用的俱两;</li><li>我們所說的內(nèi)存泄露饱狂,只針對(duì)堆內(nèi)存,他們存放的就是引用指向的對(duì)象實(shí)體宪彩。</li></ul>

Java 常用垃圾回收機(jī)制

<ul><li>引用計(jì)數(shù)</li>比較古老的回收算法休讳,原理是此對(duì)象有一個(gè)引用,即增加一個(gè)計(jì)數(shù)尿孔,刪除一個(gè)引用則減少一個(gè)計(jì)數(shù)俊柔,垃圾回收時(shí)只用收集計(jì)數(shù)為 0 的對(duì)象,此算法最致命的是無法處理循環(huán)引用的問題活合;<li>標(biāo)記-清除收集器</li>這種收集器首先遍歷對(duì)象圖并標(biāo)記可到達(dá)的對(duì)象雏婶,然后掃描堆棧以尋找未標(biāo)記對(duì)象并釋放它們的內(nèi)存,這種收集器一般使用單線程工作并會(huì)暫停其他線程操作白指,并且由于它只是清除了那些未標(biāo)記的對(duì)象留晚,而并沒有對(duì)標(biāo)記對(duì)象進(jìn)行壓縮,導(dǎo)致會(huì)產(chǎn)生大量?jī)?nèi)存碎片告嘲,從而浪費(fèi)內(nèi)存错维;<li>標(biāo)記-壓縮收集器</li>有時(shí)也叫標(biāo)記-清除-壓縮收集器,與標(biāo)記-清除收集器有相同的標(biāo)記階段橄唬,但是在第二階段則把標(biāo)記對(duì)象復(fù)制到堆棧的新域中以便壓縮堆棧赋焕,這種收集器也會(huì)暫停其他操作;<li>復(fù)制收集器(半空間)</li>這種收集器將堆棧分為兩個(gè)域仰楚,常稱為半空間隆判,每次僅使用一半的空間,JVM 生成的新對(duì)象則放在另一半空間中缸血,GC 運(yùn)行時(shí)它把可到達(dá)對(duì)象復(fù)制到另一半空間從而壓縮了堆棧蜜氨,這種方法適用于短生存期的對(duì)象,持續(xù)復(fù)制長(zhǎng)生存期的對(duì)象則導(dǎo)致效率降低捎泻,并且對(duì)于指定大小堆來說需要兩倍大小的內(nèi)存飒炎,因?yàn)槿魏螘r(shí)候都只使用其中的一半;<li>增量收集器</li>增量收集器把堆棧分為多個(gè)域笆豁,每次僅從一個(gè)域收集垃圾郎汪,也可理解為把堆棧分成一小塊一小塊赤赊,每次僅對(duì)某一個(gè)塊進(jìn)行垃圾收集,這就只會(huì)引起較小的應(yīng)用程序中斷時(shí)間煞赢,使得用戶一般不能覺察到垃圾收集器運(yùn)行抛计;<li>分代收集器</li>復(fù)制收集器的缺點(diǎn)是每次收集時(shí)所有的標(biāo)記對(duì)象都要被拷貝,從而導(dǎo)致一些生命周期很長(zhǎng)的對(duì)象被來回拷貝多次照筑,消耗大量的時(shí)間吹截,而分代收集器則可解決這個(gè)問題,分代收集器把堆棧分為兩個(gè)或多個(gè)域用以存放不同壽命的對(duì)象凝危,JVM 生成的新對(duì)象一般放在其中的某個(gè)域中波俄,過一段時(shí)間,繼續(xù)存在的對(duì)象(非短命對(duì)象)將轉(zhuǎn)入更長(zhǎng)壽命的域中蛾默,分代收集器對(duì)不同的域使用不同的算法以優(yōu)化性能懦铺。</ul>

Java/Android 4.4 版本之下 Dalvik 虛擬機(jī)分析

Dalvik 堆簡(jiǎn)介

這里寫圖片描述

上圖為 Dalvik 虛擬機(jī)的 Java 堆描述(出自:Dalvik虛擬機(jī)Java堆創(chuàng)建過程分析),如上圖所示支鸡,在 Dalvik 虛擬機(jī)中冬念,Java 堆實(shí)際上是由一個(gè) Active 堆和一個(gè) Zygote 堆組成的,其中 Zygote 堆用來管理 Zygote 進(jìn)程在啟動(dòng)過程中預(yù)加載和創(chuàng)建的各種對(duì)象牧挣,而 Active 堆是在 Zygote 進(jìn)程 fork 第一個(gè)子進(jìn)程之前創(chuàng)建的急前,應(yīng)用進(jìn)程都是通過 Zygote 進(jìn)程 fork 出來的(相關(guān)函數(shù)為 ZygoteInit.main 函數(shù):Android TransactionTooLargeException 解析,思考與監(jiān)控方案)浸踩,之后無論是 Zygote 進(jìn)程還是其子進(jìn)程叔汁,都在 Active 堆上進(jìn)行對(duì)象分配和釋放,這樣做的目的是使得 Zygote 進(jìn)程和其子進(jìn)程最大限度地共享 Zygote 堆所占用的內(nèi)存检碗。上面講到應(yīng)用程序進(jìn)程是由 Zygote 進(jìn)程 fork 出來的,也就是說應(yīng)用程序進(jìn)程使用了一種寫時(shí)拷貝技術(shù)(COW)來復(fù)制 Zygote 進(jìn)程的地址空間码邻,這意味著一開始的時(shí)候折剃,應(yīng)用程序進(jìn)程和 Zygote 進(jìn)程共享了同一個(gè)用來分配對(duì)象的堆,然而當(dāng) Zygote 進(jìn)程或者應(yīng)用程序進(jìn)程對(duì)該堆進(jìn)行寫操作時(shí)像屋,內(nèi)核才會(huì)執(zhí)行真正的拷貝操作怕犁,使得 Zygote 進(jìn)程和應(yīng)用程序進(jìn)程分別擁有自己的一份拷貝〖狠海拷貝是一件費(fèi)時(shí)費(fèi)力的事情奏甫,因此為了盡量地避免拷貝,Dalvik 虛擬機(jī)將自己的堆劃分為兩部分凌受,事實(shí)上 Dalvik 虛擬機(jī)的堆最初是只有一個(gè)的阵子,也就是 Zygote 進(jìn)程在啟動(dòng)過程中創(chuàng)建 Dalvik 虛擬機(jī)的時(shí)候只有一個(gè)堆,但是當(dāng) Zygote 進(jìn)程在 fork 第一個(gè)應(yīng)用程序進(jìn)程之前會(huì)將已經(jīng)使用了的那部分堆內(nèi)存劃分為一部分胜蛉,還沒有使用的堆內(nèi)存劃分為另外一部分挠进,前者就稱為 Zygote 堆色乾,后者就稱為 Active 堆。以后無論是 Zygote 進(jìn)程還是應(yīng)用程序進(jìn)程领突,當(dāng)它們需要分配對(duì)象的時(shí)候暖璧,都在 Active 堆上進(jìn)行,這樣就可以使得 Zygote 堆被應(yīng)用進(jìn)程和 Zygote 進(jìn)程共享從而盡可能少地被執(zhí)行寫操作君旦,所以就可以減少執(zhí)行寫時(shí)的拷貝操作澎办。在 Zygote 堆里面分配的對(duì)象其實(shí)主要就是 Zygote 進(jìn)程在啟動(dòng)過程中預(yù)加載的類、資源和對(duì)象金砍,這意味著這些預(yù)加載的類局蚀、資源和對(duì)象可以在 Zygote 進(jìn)程和應(yīng)用程序進(jìn)程中做到長(zhǎng)期共享,這樣既能減少拷貝操作還能減少對(duì)內(nèi)存的需求(出自:Dalvik虛擬機(jī)垃圾收集機(jī)制簡(jiǎn)要介紹和學(xué)習(xí)計(jì)劃)捞魁。

Dalvik 分配內(nèi)存過程分析

這里寫圖片描述

上圖就是 Dalvik VM 為新創(chuàng)建對(duì)象分配內(nèi)存的過程(出自:Dalvik虛擬機(jī)為新創(chuàng)建對(duì)象分配內(nèi)存的過程分析)至会,我們來看看分配的具體步驟:<ol><li>Dalvik 虛擬機(jī)實(shí)現(xiàn)了一個(gè) dvmAllocObject 函數(shù),每當(dāng) Dalvik 虛擬機(jī)需要為對(duì)象分配內(nèi)存時(shí)谱俭,就會(huì)調(diào)用函數(shù) dvmAllocObject奉件,例如,當(dāng) Dalvik 虛擬機(jī)的解釋器遇到一個(gè) new 指令時(shí)昆著,它就會(huì)調(diào)用函數(shù) dvmAllocObject县貌;</li><li>函數(shù) dvmAllocObject 調(diào)用函數(shù) dvmMalloc 從 Java 堆中分配一塊指定大小的內(nèi)存給新創(chuàng)建的對(duì)象使用,如果分配成功凑懂,那么接下來就先使用宏 DVM_OBJECT_INIT 來初始化新創(chuàng)建對(duì)象的成員變量 clazz煤痕,使得新創(chuàng)建的對(duì)象可以與某個(gè)特定的類關(guān)聯(lián)起來,接著再調(diào)用函數(shù) dvmTrackAllocation 記錄當(dāng)前的內(nèi)存分配信息接谨,以便通知 DDMS摆碉。函數(shù) dvmMalloc 返回的只是一塊內(nèi)存地址,這是沒有類型的脓豪,但是由于每一個(gè) Java 對(duì)象都是從 Object 類繼承下來的巷帝,因此函數(shù) dvmAllocObject 可以將獲得的沒有類型的內(nèi)存塊強(qiáng)制轉(zhuǎn)換為一個(gè) Object 對(duì)象;</li><li> dvmMalloc 函數(shù)接著調(diào)用到了另一個(gè)函數(shù) tryMalloc 扫夜,真正執(zhí)行內(nèi)存分配操作的就是這個(gè) tryMalloc 函數(shù)楞泼,dvmMalloc 函數(shù)操作如果分配內(nèi)存成功,則記錄當(dāng)前線程成功分配的內(nèi)存字節(jié)數(shù)和對(duì)象數(shù)等信息笤闯;否則的話堕阔,就記錄當(dāng)前線程失敗分配的內(nèi)存字節(jié)數(shù)和對(duì)象等信息,方便通過 DDMS 等工具對(duì)內(nèi)存使用信息進(jìn)行統(tǒng)計(jì)颗味,同時(shí)會(huì)調(diào)用函數(shù) throwOOME 拋出一個(gè) OOM 異常超陆;</li>

void* dvmMalloc(size_t size, int flags)  
{  
    void *ptr;  
  
    dvmLockHeap();  
  
    /* Try as hard as possible to allocate some memory. 
     */  
    ptr = tryMalloc(size);  
    if (ptr != NULL) {  
        /* We've got the memory. 
         */  
        if (gDvm.allocProf.enabled) {  
            Thread* self = dvmThreadSelf();  
            gDvm.allocProf.allocCount++;  
            gDvm.allocProf.allocSize += size;  
            if (self != NULL) {  
                self->allocProf.allocCount++;  
                self->allocProf.allocSize += size;  
            }  
        }  
    } else {  
        /* The allocation failed. 
         */  
  
        if (gDvm.allocProf.enabled) {  
            Thread* self = dvmThreadSelf();  
            gDvm.allocProf.failedAllocCount++;  
            gDvm.allocProf.failedAllocSize += size;  
            if (self != NULL) {  
                self->allocProf.failedAllocCount++;  
                self->allocProf.failedAllocSize += size;  
            }  
        }  
    }  
  
    dvmUnlockHeap();  
  
    if (ptr != NULL) {  
        /* 
         * If caller hasn't asked us not to track it, add it to the 
         * internal tracking list. 
         */  
        if ((flags & ALLOC_DONT_TRACK) == 0) {  
            dvmAddTrackedAlloc((Object*)ptr, NULL);  
        }  
    } else {  
        /* 
         * The allocation failed; throw an OutOfMemoryError. 
         */  
        throwOOME();  
    }  
  
    return ptr;  
}  

<li>再來具體分析一下函數(shù) tryMalloc,tryMalloc 會(huì)調(diào)用函數(shù) dvmHeapSourceAlloc 在 Java 堆上分配指定大小的內(nèi)存脱衙,如果分配成功侥猬,那么就將分配得到的地址直接返回給調(diào)用者了例驹,函數(shù) dvmHeapSourceAlloc 在不改變 Java 堆當(dāng)前大小的前提下進(jìn)行內(nèi)存分配,這是屬于輕量級(jí)的內(nèi)存分配動(dòng)作退唠;</li><li>如果上一步內(nèi)存分配失敗鹃锈,這時(shí)候就需要執(zhí)行一次 GC 了,不過如果 GC 線程已經(jīng)在運(yùn)行中瞧预,即 gDvm.gcHeap->gcRunning 的值等于 true屎债,那么就直接調(diào)用函數(shù) dvmWaitForConcurrentGcToComplete 等到 GC 執(zhí)行完成;否則的話垢油,就需要調(diào)用函數(shù) gcForMalloc 來執(zhí)行一次 GC 了盆驹,參數(shù) false 表示不要回收軟引用對(duì)象引用的對(duì)象;</li>

static void *tryMalloc(size_t size)  
{  
    void *ptr;  
    ......  
  
    ptr = dvmHeapSourceAlloc(size);  
    if (ptr != NULL) {  
        return ptr;  
    }  
  
    if (gDvm.gcHeap->gcRunning) {  
        ......  
        dvmWaitForConcurrentGcToComplete();  
    } else {  
        ......  
        gcForMalloc(false);  
    }  
  
    ptr = dvmHeapSourceAlloc(size);  
    if (ptr != NULL) {  
        return ptr;  
    }  
  
    ptr = dvmHeapSourceAllocAndGrow(size);  
    if (ptr != NULL) {  
        ......  
        return ptr;  
    }  
  
    gcForMalloc(true);  
    ptr = dvmHeapSourceAllocAndGrow(size);  
    if (ptr != NULL) {  
        return ptr;  
    }  
     
    ......  
  
    return NULL;  
}  

<li>GC 執(zhí)行完畢后滩愁,再次調(diào)用函數(shù) dvmHeapSourceAlloc 嘗試輕量級(jí)的內(nèi)存分配操作躯喇,如果分配成功,那么就將分配得到的地址直接返回給調(diào)用者了硝枉;</li><li>如果上一步內(nèi)存分配失敗廉丽,這時(shí)候就得考慮先將 Java 堆的當(dāng)前大小設(shè)置為 Dalvik 虛擬機(jī)啟動(dòng)時(shí)指定的 Java 堆最大值再進(jìn)行內(nèi)存分配了,這是通過調(diào)用函數(shù) dvmHeapSourceAllocAndGrow 來實(shí)現(xiàn)的妻味;</li><li>如果調(diào)用函數(shù) dvmHeapSourceAllocAndGrow 分配內(nèi)存成功正压,則直接將分配得到的地址直接返回給調(diào)用者了;</li><li>如果上一步內(nèi)存分配還是失敗责球,這時(shí)候就得出狠招了焦履,再次調(diào)用函數(shù) gcForMalloc 來執(zhí)行 GC,不過這次參數(shù)為 true 表示要回收軟引用對(duì)象引用的對(duì)象雏逾;</li><li>上一步 GC 執(zhí)行完畢嘉裤,再次調(diào)用函數(shù) dvmHeapSourceAllocAndGrow 進(jìn)行內(nèi)存分配,這是最后一次努力了栖博,如果還分配內(nèi)存不成功那就是 OOM 了价脾。</li></ol>

Dalvik GC 策略分析

不同語言平臺(tái)進(jìn)行標(biāo)記回收內(nèi)存的算法是不一樣的,Java 則是采用的 GC-Root 標(biāo)記回收算法笛匙,在 Android 4,4 之下也是和 Java 一樣的機(jī)制(Android 4.4 和之后都是使用了 ART,和dalvik GC 有不同的地方)犀变,下面這張來自 Google IO 2011 大會(huì)的圖就很好的展示了Android 4.4 版本之下的回收策略:

  
這里寫圖片描述

圖中的每個(gè)圓節(jié)點(diǎn)代表對(duì)象的內(nèi)存資源妹孙,箭頭代表可達(dá)路徑,當(dāng)一個(gè)圓節(jié)點(diǎn)和 GC Roots 存在可達(dá)的路徑時(shí)获枝,表示當(dāng)前它指向的內(nèi)存資源正在被引用蠢正,虛擬機(jī)是無法對(duì)其進(jìn)行回收的(圖中的黃色節(jié)點(diǎn));反過來省店,如果當(dāng)前的圓節(jié)點(diǎn)和 GC Roots 不存在可達(dá)路徑嚣崭,則意味著這塊對(duì)象的內(nèi)存資源不再被程序引用笨触,系統(tǒng)虛擬機(jī)可以在 GC 的時(shí)候?qū)⑵鋬?nèi)存回收掉。具體點(diǎn)來說雹舀,Java/Android 的內(nèi)存垃圾回收機(jī)制是從程序的主要運(yùn)行對(duì)象(如靜態(tài)對(duì)象/寄存器/棧上指向的內(nèi)存對(duì)象等芦劣,對(duì)應(yīng)上面的 GC Roots)開始檢查調(diào)用鏈,當(dāng)遍歷一遍后得到上述這些無法回收的對(duì)象和他們所引用的對(duì)象鏈組成無法回收的對(duì)象集合说榆,而剩余其他的孤立對(duì)象(集)就作為垃圾被 GC 回收虚吟。GC 為了能夠正確釋放對(duì)象,必須監(jiān)控每一個(gè)對(duì)象的運(yùn)行狀態(tài)签财,包括對(duì)象的申請(qǐng)串慰、引用、被引用唱蒸、賦值等邦鲫。監(jiān)視對(duì)象狀態(tài)是為了更加準(zhǔn)確地、及時(shí)地釋放對(duì)象神汹,而釋放對(duì)象的根本原則就是該對(duì)象不再被引用庆捺。

  上面介紹了 GC 的回收機(jī)制,那么接下來?yè)?jù)此說一下什么是內(nèi)存泄漏慎冤,從抽象定義上講疼燥,Java/Android 平臺(tái)的內(nèi)存泄漏是指沒有用的對(duì)象資源仍然和 GC Roots 保持可達(dá)路徑,導(dǎo)致系統(tǒng)無法進(jìn)行回收蚁堤,具體一點(diǎn)講就是醉者,通過 GC Roots 的調(diào)用鏈可以遍歷到這個(gè)沒有被使用的對(duì)象,導(dǎo)致該資源無法進(jìn)行釋放披诗。最常見的比如撬即,Android 中的 Activity 中創(chuàng)建一個(gè)內(nèi)部類 Handler 用來處理多線程的消息,而內(nèi)部類會(huì)持有外部類的引用呈队,所以該 Handler 的對(duì)象會(huì)持有 Activity 的引用剥槐,而如果這個(gè) Handler 對(duì)象被子線程持有,子線程正在進(jìn)行耗時(shí)的操作沒法在短時(shí)間內(nèi)執(zhí)行完成宪摧,那么一系列的引用鏈導(dǎo)致 Activity 關(guān)閉之后一直無法被釋放粒竖,重復(fù)地打開關(guān)閉這個(gè) Activity 會(huì)造成這些 Activity 的對(duì)象一直在內(nèi)存當(dāng)中,最終達(dá)到一定程度之后會(huì)產(chǎn)生 OOM 異常几于。
  在 Java/Android 中蕊苗,雖然我們有幾個(gè)函數(shù)可以訪問 GC,例如運(yùn)行GC的函數(shù) System.gc()沿彭,但是根據(jù) Java 語言規(guī)范定義朽砰,該函數(shù)不保證 JVM 的垃圾收集器一定會(huì)馬上執(zhí)行。因?yàn)椴煌?JVM 實(shí)現(xiàn)者可能使用不同的算法管理 GC,通常 GC 的線程的優(yōu)先級(jí)別較低瞧柔。JVM 調(diào)用 GC 的策略也有很多種漆弄,有的是內(nèi)存使用到達(dá)一定程度時(shí) GC 才開始工作,也有定時(shí)執(zhí)行的造锅,有的是平緩執(zhí)行GC撼唾,也有的是中斷式執(zhí)行GC,但通常來說我們開發(fā)者不需要關(guān)心這些备绽。

Dalvik GC 日志分析

上面介紹到券坞,雖然我們有幾個(gè)函數(shù)可以訪問 GC,但是該函數(shù)不會(huì)保證 GC 操作會(huì)立馬執(zhí)行肺素,那么我怎么去監(jiān)聽系統(tǒng)的 GC 過程來實(shí)時(shí)分析當(dāng)前的內(nèi)存狀態(tài)呢恨锚?其實(shí)很簡(jiǎn)單,Android 4.4 版本之下系統(tǒng) Dalvik 每進(jìn)行一次 GC 操作都會(huì)在 LogCat 中打印一條對(duì)應(yīng)的日志倍靡,我們只需要去分析這條日志就可以了猴伶,日志的基本格式如下:

D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>,  <Pause_time>

這段日志分為 4 個(gè)部分:<ul>
<li>首先是第一部分 GC_Reason,就是觸發(fā)這次 GC 的原因塌西,一般情況下有以下幾種原因:</li><ul><li> GC_CONCURRENT </li>當(dāng)我們應(yīng)用程序的堆內(nèi)存快要滿的時(shí)候他挎,系統(tǒng)會(huì)自動(dòng)觸發(fā) GC 操作來釋放內(nèi)存团南;<li> GC_FOR_MALLOC </li>當(dāng)我們的應(yīng)用程序需要分配更多內(nèi)存左医,可是現(xiàn)有內(nèi)存已經(jīng)不足的時(shí)候准浴,系統(tǒng)會(huì)進(jìn)行 GC 操作來釋放內(nèi)存设易;<li> GC_HPROF_DUMP_HEAP </li>當(dāng)生成內(nèi)存分析 HPROF 文件的時(shí)候,系統(tǒng)會(huì)進(jìn)行 GC 操作唁毒,我們下面會(huì)分析一下 HPROF 文件溪厘;<li> GC_EXPLICIT </li>這種情況就是我們剛才提到過的条获,主動(dòng)通知系統(tǒng)去進(jìn)行GC操作饰剥,比如調(diào)用 System.gc() 方法來通知系統(tǒng)殊霞,或者在 DDMS 中,通過工具按鈕也是可以顯式地告訴系統(tǒng)進(jìn)行 GC 操作的汰蓉。</ul>
<li>第二部分 Amount_freed绷蹲,表示系統(tǒng)通過這次 GC 操作釋放了多少的內(nèi)存;</li>
<li>第三部分 Heap_stats顾孽,代表當(dāng)前內(nèi)存的空閑比例以及使用情況(活動(dòng)對(duì)象所占內(nèi)存 / 當(dāng)前程序總內(nèi)存)祝钢;</li>
<li>第四部分 Pause_time,代表了這次 GC 操作導(dǎo)致應(yīng)用程序暫停的時(shí)間若厚,在 Android 2.3 版本之前 GC 操作是不能并發(fā)執(zhí)行的太颤,所以如果當(dāng)系統(tǒng)正在 GC 的時(shí)候,應(yīng)用程序只能阻塞等待 GC 結(jié)束盹沈,GC 的時(shí)間基本在幾百毫秒左右,所以用戶會(huì)感覺到略微明顯的卡頓,體驗(yàn)不好乞封,在 Android 2.3 以及之后到 4.4 版本之前做裙,Dalvik GC 的操作改成了并發(fā)執(zhí)行,也就是說 GC 的操作不會(huì)影響到主應(yīng)用程序的正常運(yùn)行肃晚,但是 GC 操作的開始和結(jié)束仍然會(huì)短暫的阻塞一段時(shí)間锚贱,不過時(shí)間上面就已經(jīng)短到讓用戶無法察覺到了。</li>
</ul>

Android 4.4 及以上 ART 分析

ART 堆簡(jiǎn)介

這里寫圖片描述

上圖為 ART 堆的描述(圖片出自:ART運(yùn)行時(shí)垃圾收集機(jī)制簡(jiǎn)要介紹和學(xué)習(xí)計(jì)劃)关串,ART 也涉及到類似于 Dalvik 虛擬機(jī)的 Zygote 堆拧廊、Active 堆、Card Table晋修、Heap Bitmap 和 Mark Stack 等概念吧碾。從圖中可以看到,ART 運(yùn)行時(shí)堆劃分為四個(gè)空間墓卦,分別是 Image Space倦春、Zygote Space、Allocation Space 和 Large Object Space落剪,其中 Image Space睁本、Zygote Space 和 Allocation Space 是在地址上連續(xù)的空間,稱為 Continuous Space忠怖,而 Large Object Space 是一些離散地址的集合呢堰,用來分配一些大對(duì)象,稱為 Discontinuous Space凡泣。
  在 Image Space 和 Zygote Space 之間枉疼,隔著一段用來映射 system@framework@boot.art@classes.oat 文件的內(nèi)存,system@framework@boot.art@classes.oat 是一個(gè) OAT 文件问麸,它是由在系統(tǒng)啟動(dòng)類路徑中的所有 .dex 文件翻譯得到的往衷,而 Image Space 空間就包含了那些需要預(yù)加載的系統(tǒng)類對(duì)象,這意味著需要預(yù)加載的類對(duì)象是在生成 system@framework@boot.art@classes.oat 這個(gè) OAT 文件的時(shí)候創(chuàng)建并且保存在文件 system@framework@boot.art@classes.dex 中严卖,以后只要系統(tǒng)啟動(dòng)類路徑中的 .dex 文件不發(fā)生變化(即不發(fā)生更新升級(jí))席舍,那么以后每次系統(tǒng)啟動(dòng)只需要將文件 system@framework@boot.art@classes.dex 直接映射到內(nèi)存即可,省去了創(chuàng)建各個(gè)類對(duì)象的時(shí)間哮笆。之前使用 Dalvik 虛擬機(jī)作為應(yīng)用程序運(yùn)行時(shí)的時(shí)候来颤,每次系統(tǒng)啟動(dòng)都需要為那些預(yù)加載的系統(tǒng)類創(chuàng)建類對(duì)象,而雖然 ART 運(yùn)行時(shí)第一次啟動(dòng)會(huì)和 Dalvik 一樣比較慢稠肘,但是以后啟動(dòng)實(shí)際上會(huì)快不少福铅。由于 system@framework@boot.art@classes.dex 文件保存的是一些預(yù)先創(chuàng)建的對(duì)象,并且這些對(duì)象之間可能會(huì)互相引用项阴,因此我們必須保證 system@framework@boot.art@classes.dex 文件每次加載到內(nèi)存的地址都是固定的滑黔,這個(gè)固定的地址保存在 system@framework@boot.art@classes.dex 文件頭部的一個(gè) Image Header 中,此外 system@framework@boot.art@classes.dex 文件也依賴于 system@framework@boot.art@classes.oat 文件,所以也會(huì)將后者固定加載到 Image Space 的末尾略荡。
  Zygote Space 和 Allocation Space 與上面講到的 Dalvik 虛擬機(jī)中的 Zygote 堆和 Active 堆的作用是一樣的庵佣,Zygote Space 在 Zygote 進(jìn)程和應(yīng)用程序進(jìn)程之間共享的,而 Allocation Space 則是每個(gè)進(jìn)程獨(dú)占的汛兜。同樣的 Zygote 進(jìn)程一開始只有一個(gè) Image Space 和一個(gè) Zygote Space巴粪,在 Zygote 進(jìn)程 fork 出第一個(gè)子進(jìn)程之前,就會(huì)把 Zygote Space 一分為二粥谬,原來的已經(jīng)被使用的那部分堆還叫 Zygote Space肛根,而未使用的那部分堆就叫 Allocation Space,以后的對(duì)象都在新分出來的 Allocation Space 上分配漏策,通過上述這種方式派哲,就可以使得 Image Space 和 Zygote Space 在 Zygote 進(jìn)程和應(yīng)用程序進(jìn)程之間進(jìn)行共享,而 Allocation Space 就每個(gè)進(jìn)程都獨(dú)立地?fù)碛幸环萦寸瑁?Dalvik 同樣既能減少拷貝操作還能減少對(duì)內(nèi)存的需求狮辽。有一點(diǎn)需要注意的是雖然 Image Space 和 Zygote Space 都是在 Zygote 進(jìn)程和應(yīng)用程序進(jìn)程之間進(jìn)行共享的,但是前者的對(duì)象只創(chuàng)建一次而后者的對(duì)象需要在系統(tǒng)每次啟動(dòng)時(shí)根據(jù)運(yùn)行情況都重新創(chuàng)建一遍(出自:ART運(yùn)行時(shí)垃圾收集機(jī)制簡(jiǎn)要介紹和學(xué)習(xí)計(jì)劃)巢寡。
  ART 運(yùn)行時(shí)提供了兩種 Large Object Space 實(shí)現(xiàn)喉脖,其中一種實(shí)現(xiàn)和 Continuous Space 的實(shí)現(xiàn)類似,預(yù)先分配好一塊大的內(nèi)存空間抑月,然后再在上面為對(duì)象分配內(nèi)存塊树叽,不過這種方式實(shí)現(xiàn)的 Large Object Space 不像 Continuous Space 通過 C 庫(kù)的內(nèi)塊管理接口來分配和釋放內(nèi)存,而是自己維護(hù)一個(gè) Free List谦絮,每次為對(duì)象分配內(nèi)存時(shí)题诵,都是從這個(gè) Free List 找到合適的空閑的內(nèi)存塊來分配,釋放內(nèi)存的時(shí)候层皱,也是將要釋放的內(nèi)存添加到該 Free List 去性锭;另外一種 Large Object Space 實(shí)現(xiàn)是每次為對(duì)象分配內(nèi)存時(shí),都單獨(dú)為其映射一新的內(nèi)存叫胖,也就是說草冈,為每一個(gè)對(duì)象分配的內(nèi)存塊都是相互獨(dú)立的,這種實(shí)現(xiàn)方式相比上面介紹的 Free List 實(shí)現(xiàn)方式更簡(jiǎn)單一些瓮增。在 Android 4.4 中怎棱,ART 運(yùn)行時(shí)使用的是后一種實(shí)現(xiàn)方式,為每一對(duì)象映射一塊獨(dú)立的內(nèi)存塊的 Large Object Space 實(shí)現(xiàn)稱為 LargeObjectMapSpace绷跑,它與 Free List 方式的實(shí)現(xiàn)都是繼承于類 LargeObjectSpace拳恋,LargeObjectSpace 又分別繼承了 DiscontinuousSpace 和 AllocSpace,因此我們就可以知道砸捏,LargeObjectMapSpace 描述的是一個(gè)在地址空間上不連續(xù)的 Large Object Space谬运。

ART 分配內(nèi)存過程分析

這里寫圖片描述

上圖就是 ART 為新創(chuàng)建對(duì)象分配內(nèi)存的過程(出自:ART運(yùn)行時(shí)為新創(chuàng)建對(duì)象分配內(nèi)存的過程分析)隙赁,可以看到 ART 為新創(chuàng)建對(duì)象分配內(nèi)存的過程和 Dalvik VM 幾乎是一樣的,區(qū)別僅僅在于垃圾收集的方式和策略不一樣吩谦。
  ART 運(yùn)行時(shí)為從 DEX 字節(jié)碼翻譯得到的 Native 代碼提供的一個(gè)函數(shù)調(diào)用表中鸳谜,有一個(gè) pAllocObject 接口是用來分配對(duì)象的,當(dāng) ART 運(yùn)行時(shí)以 Quick 模式運(yùn)行在 ARM 體系結(jié)構(gòu)時(shí)式廷,上述提到的 pAllocObject 接口由函數(shù) art_quick_alloc_object 來實(shí)現(xiàn),art_quick_alloc_object 是一段匯編代碼芭挽,最終經(jīng)過一系列的調(diào)用之后最終會(huì)調(diào)用 ART 運(yùn)行時(shí)內(nèi)部的 Heap 對(duì)象的成員函數(shù) AllocObject 在堆上分配對(duì)象(具體的過程:ART運(yùn)行時(shí)為新創(chuàng)建對(duì)象分配內(nèi)存的過程分析)滑废,其中要分配的大小保存在當(dāng)前 Class 對(duì)象的成員變量 object_size_ 中。 Heap 類的成員函數(shù) AllocObject 首先是要確定要在哪個(gè) Space 上分配內(nèi)存袜爪,可以分配內(nèi)存的 Space 有三個(gè)蠕趁,分別 Zygote Space、Allocation Space 和 Large Object Space辛馆,不過 Zygote Space 在還沒有劃分出 Allocation Space 之前就在 Zygote Space 上分配俺陋,而當(dāng) Zygote Space 劃分出 Allocation Space 之后,就只能在 Allocation Space 上分配昙篙,同時(shí) Heap 類的成員變量 alloc_space_ 在 Zygote Space 還沒有劃分出 Allocation Space 之前指向 Zygote Space腊状,而劃分之后就指向 Allocation Space,Large Object Space 則始終由 Heap 類的成員變量 large_object_space_ 指向苔可。只要滿足以下三個(gè)條件就在 Large Object Space 上分配缴挖,否則就在 Zygote Space 或者 Allocation Space 上分配:<ol><li>請(qǐng)求分配的內(nèi)存大于等于 Heap 類的成員變量 large_object_threshold_ 指定的值,這個(gè)值等于 3 * kPageSize焚辅,即 3 個(gè)頁(yè)面的大杏澄荨;</li><li>已經(jīng)從 Zygote Space 劃分出 Allocation Space同蜻,即 Heap 類的成員變量 have_zygote_space_ 的值等于 true棚点;</li><li>被分配的對(duì)象是一個(gè)原子類型數(shù)組,即 byte 數(shù)組湾蔓、int 數(shù)組和 boolean 數(shù)組等瘫析。</li></ol>確定好要在哪個(gè) Space 上分配內(nèi)存之后,就可以調(diào)用 Heap 類的成員函數(shù) Allocate 進(jìn)行分配了卵蛉,如果分配成功颁股,Heap 類的成員函數(shù) Allocate 就返回新分配的對(duì)象并且將該對(duì)象保存在變量 obj 中,接下來再會(huì)做三件事情:<ol><li>調(diào)用 Object 類的成員函數(shù) SetClass 設(shè)置新分配對(duì)象 obj 的類型傻丝;</li><li>調(diào)用 Heap 類的成員函數(shù) RecordAllocation 記錄當(dāng)前的內(nèi)存分配狀況甘有;</li><li>檢查當(dāng)前已經(jīng)分配出去的內(nèi)存是否已經(jīng)達(dá)到由 Heap 類的成員變量 concurrent_start_bytes_ 設(shè)定的閥值,如果已經(jīng)達(dá)到葡缰,那么就調(diào)用 Heap 類的成員函數(shù) RequestConcurrentGC 通知 GC 執(zhí)行一次并行 GC亏掀。</li></ol>另一方面如果 Heap 類的成員函數(shù) Allocate 分配內(nèi)存失敗忱反,則 Heap 類的成員函數(shù) AllocObject 拋出一個(gè) OOM 異常。Heap 類的 AllocObject 函數(shù)又會(huì)調(diào)用到成員函數(shù) Allocate:

mirror::Object* Heap::AllocObject(Thread* self, mirror::Class* c, size_t byte_count) {  
  ......  
  
  mirror::Object* obj = NULL;  
  size_t bytes_allocated = 0;  
  ......  
  
  bool large_object_allocation =  
      byte_count >= large_object_threshold_ && have_zygote_space_ && c->IsPrimitiveArray();  
  if (UNLIKELY(large_object_allocation)) {  
    obj = Allocate(self, large_object_space_, byte_count, &bytes_allocated);  
    ......  
  } else {  
    obj = Allocate(self, alloc_space_, byte_count, &bytes_allocated);  
    ......  
  }  
  
  if (LIKELY(obj != NULL)) {  
    obj->SetClass(c);  
    ......  
  
    RecordAllocation(bytes_allocated, obj);  
    ......  
  
    if (UNLIKELY(static_cast<size_t>(num_bytes_allocated_) >= concurrent_start_bytes_)) {  
      ......  
      SirtRef<mirror::Object> ref(self, obj);  
      RequestConcurrentGC(self);  
    }  
    ......  
  
    return obj;  
  } else {  
    ......  
    self->ThrowOutOfMemoryError(oss.str().c_str());  
    return NULL;  
  }  
}  

函數(shù) Allocate 首先調(diào)用成員函數(shù) TryToAllocate 嘗試在不執(zhí)行 GC 的情況下進(jìn)行內(nèi)存分配滤愕,如果分配失敗再調(diào)用成員函數(shù) AllocateInternalWithGc 進(jìn)行帶 GC 的內(nèi)存分配温算,Allocate 是一個(gè)模板函數(shù),不同類型的 Space 會(huì)導(dǎo)致調(diào)用不同重載的成員函數(shù) TryToAllocate 進(jìn)行不帶 GC 的內(nèi)存分配间影。雖然可以用來分配內(nèi)存的 Space 有 Zygote Space注竿、Allocation Space 和 Large Object Space 三個(gè),但是前兩者的類型是相同的魂贬,因此實(shí)際上只有兩個(gè)不同重載版本的成員函數(shù) TryToAllocate巩割,它們的實(shí)現(xiàn)如下所示:

inline mirror::Object* Heap::TryToAllocate(Thread* self, space::AllocSpace* space, size_t alloc_size,  
                                           bool grow, size_t* bytes_allocated) {  
  if (UNLIKELY(IsOutOfMemoryOnAllocation(alloc_size, grow))) {  
    return NULL;  
  }  
  return space->Alloc(self, alloc_size, bytes_allocated);  
}  
  
// DlMallocSpace-specific version.  
inline mirror::Object* Heap::TryToAllocate(Thread* self, space::DlMallocSpace* space, size_t alloc_size,  
                                           bool grow, size_t* bytes_allocated) {  
  if (UNLIKELY(IsOutOfMemoryOnAllocation(alloc_size, grow))) {  
    return NULL;  
  }  
  if (LIKELY(!running_on_valgrind_)) {  
    return space->AllocNonvirtual(self, alloc_size, bytes_allocated);  
  } else {  
    return space->Alloc(self, alloc_size, bytes_allocated);  
  }  
} 

Heap 類兩個(gè)重載版本的成員函數(shù) TryToAllocate 的實(shí)現(xiàn)邏輯都幾乎是相同的,首先是調(diào)用另外一個(gè)成員函數(shù) IsOutOfMemoryOnAllocation 判斷分配請(qǐng)求的內(nèi)存后是否會(huì)超過堆的大小限制付燥,如果超過則分配失斝浮;否則的話再在指定的 Space 進(jìn)行內(nèi)存分配键科。函數(shù)IsOutOfMemoryOnAllocation的實(shí)現(xiàn)如下所示:

inline bool Heap::IsOutOfMemoryOnAllocation(size_t alloc_size, bool grow) {  
  size_t new_footprint = num_bytes_allocated_ + alloc_size;  
  if (UNLIKELY(new_footprint > max_allowed_footprint_)) {  
    if (UNLIKELY(new_footprint > growth_limit_)) {  
      return true;  
    }  
    if (!concurrent_gc_) {  
      if (!grow) {  
        return true;  
      } else {  
        max_allowed_footprint_ = new_footprint;  
      }  
    }  
  }  
  return false;  
}  

成員變量 num_bytes_allocated_ 描述的是目前已經(jīng)分配出去的內(nèi)存字節(jié)數(shù)闻丑,成員變量 max_allowed_footprint_ 描述的是目前堆可分配的最大內(nèi)存字節(jié)數(shù),成員變量 growth_limit_ 描述的是目前堆允許增長(zhǎng)到的最大內(nèi)存字節(jié)數(shù)勋颖,這里需要注意的一點(diǎn)是 max_allowed_footprint_ 是 Heap 類施加的一個(gè)限制嗦嗡,不會(huì)對(duì)各個(gè) Space 實(shí)際可分配的最大內(nèi)存字節(jié)數(shù)產(chǎn)生影響,并且各個(gè) Space 在創(chuàng)建的時(shí)候牙言,已經(jīng)把自己可分配的最大內(nèi)存數(shù)設(shè)置為允許使用的最大內(nèi)存字節(jié)數(shù)酸钦。如果目前堆已經(jīng)分配出去的內(nèi)存字節(jié)數(shù)再加上請(qǐng)求分配的內(nèi)存字節(jié)數(shù) new_footprint 小于等于目前堆可分配的最大內(nèi)存字節(jié)數(shù) max_allowed_footprint_,那么分配出請(qǐng)求的內(nèi)存字節(jié)數(shù)之后不會(huì)造成 OOM咱枉,因此 Heap 類的成員函數(shù) IsOutOfMemoryOnAllocation 就返回false卑硫;另一方面,如果目前堆已經(jīng)分配出去的內(nèi)存字節(jié)數(shù)再加上請(qǐng)求分配的內(nèi)存字節(jié)數(shù) new_footprint 大于目前堆可分配的最大內(nèi)存字節(jié)數(shù) max_allowed_footprint_蚕断,并且也大于目前堆允許增長(zhǎng)到的最大內(nèi)存字節(jié)數(shù) growth_limit_欢伏,那么分配出請(qǐng)求的內(nèi)存字節(jié)數(shù)之后造成 OOM,因此 Heap 類的成員函數(shù) IsOutOfMemoryOnAllocation 這時(shí)候就返回 true亿乳。
  剩下另外一種情況硝拧,目前堆已經(jīng)分配出去的內(nèi)存字節(jié)數(shù)再加上請(qǐng)求分配的內(nèi)存字節(jié)數(shù) new_footprint 大于目前堆可分配的最大內(nèi)存字節(jié)數(shù) max_allowed_footprint_,但是小于等于目前堆允許增長(zhǎng)到的最大內(nèi)存字節(jié)數(shù) growth_limit_葛假,這時(shí)候就要看情況會(huì)不會(huì)出現(xiàn) OOM 了:如果 ART 運(yùn)行時(shí)運(yùn)行在非并行 GC 的模式中障陶,即 Heap 類的成員變量 concurrent_gc_ 等于 false,那么取決于允不允許增長(zhǎng)堆的大小聊训,即參數(shù) grow 的值抱究,如果不允許,那么 Heap 類的成員函數(shù) IsOutOfMemoryOnAllocation 就返回 true带斑,表示當(dāng)前請(qǐng)求的分配會(huì)造成 OOM鼓寺,如果允許勋拟,那么 Heap 類的成員函數(shù) IsOutOfMemoryOnAllocation 就會(huì)修改目前堆可分配的最大內(nèi)存字節(jié)數(shù) max_allowed_footprint_ 并且返回 false,表示允許當(dāng)前請(qǐng)求的分配妈候,這意味著在非并行 GC 運(yùn)行模式中敢靡,如果分配內(nèi)存過程中遇到內(nèi)存不足并且當(dāng)前可分配內(nèi)存還未達(dá)到增長(zhǎng)上限時(shí),要等到執(zhí)行完成一次非并行 GC 后才能成功分配到內(nèi)存苦银,因?yàn)槊看螆?zhí)行完成 GC 之后都會(huì)按照預(yù)先設(shè)置的堆目標(biāo)利用率來增長(zhǎng)堆的大行ル省;另一方面幔虏,如果 ART 運(yùn)行時(shí)運(yùn)行在并行 GC 的模式中吓揪,那么只要當(dāng)前堆已經(jīng)分配出去的內(nèi)存字節(jié)數(shù)再加上請(qǐng)求分配的內(nèi)存字節(jié)數(shù) new_footprint 不超過目前堆允許增長(zhǎng)到的最大內(nèi)存字節(jié)數(shù) growth_limit_,那么就不管允不允許增長(zhǎng)堆的大小都認(rèn)為不會(huì)發(fā)生 OOM所计,因此 Heap 類的成員函數(shù) IsOutOfMemoryOnAllocation 就返回 false,這意味著在并行 GC 運(yùn)行模式中团秽,在分配內(nèi)存過程中遇到內(nèi)存不足主胧,并且當(dāng)前可分配內(nèi)存還未達(dá)到增長(zhǎng)上限時(shí),無需等到執(zhí)行并行 GC 后就有可能成功分配到內(nèi)存习勤,因?yàn)閷?shí)際執(zhí)行內(nèi)存分配的 Space 可分配的最大內(nèi)存字節(jié)數(shù)是足夠的踪栋。

ART GC 策略以及過程分析

在 Android 4.4 版本以及之后就使用了 ART 運(yùn)行時(shí),在安裝的時(shí)候就將應(yīng)用翻譯成機(jī)器碼執(zhí)行图毕,效率比起以前的 Dalvik 虛擬機(jī)更高夷都,但是缺點(diǎn)就是安裝之后的應(yīng)用體積變大和安裝的時(shí)間會(huì)變長(zhǎng),不過相對(duì)于優(yōu)點(diǎn)來說予颤,這點(diǎn)缺點(diǎn)不算什么囤官。ART 運(yùn)行時(shí)與 Dalvik 虛擬機(jī)一樣,都使用了 Mark-Sweep 算法進(jìn)行垃圾回收蛤虐,因此它們的垃圾回收流程在總體上是一致的党饮,但是 ART 運(yùn)行時(shí)對(duì)堆的劃分更加細(xì)致,因而在此基礎(chǔ)上實(shí)現(xiàn)了更多樣的回收策略驳庭。不同的策略有不同的回收力度刑顺,力度越大的回收策略每次回收的內(nèi)存就越多,并且它們都有各自的使用情景饲常,這樣就可以使得每次執(zhí)行 GC 時(shí)蹲堂,可以最大限度地減少應(yīng)用程序停頓:

這里寫圖片描述

上圖描述了 ART 運(yùn)行時(shí)的垃圾收集收集過程(圖片出自:ART運(yùn)行時(shí)垃圾收集(GC)過程分析),最上面三個(gè)箭頭描述觸發(fā) GC 的三種情況贝淤,左邊的流程圖描述非并行 GC 的執(zhí)行過程柒竞,右邊的流程圖描述并行 GC 的執(zhí)行流程,過程如下所示:<ul><li>非并行 GC :<ol><li>調(diào)用子類實(shí)現(xiàn)的成員函數(shù) InitializePhase 執(zhí)行 GC 初始化階段霹娄;</li><li>掛起所有的 ART 運(yùn)行時(shí)線程能犯;</li><li>調(diào)用子類實(shí)現(xiàn)的成員函數(shù) MarkingPhase 執(zhí)行 GC 標(biāo)記階段鲫骗;</li><li>調(diào)用子類實(shí)現(xiàn)的成員函數(shù) ReclaimPhase 執(zhí)行 GC 回收階段;</li><li>恢復(fù)第 2 步掛起的 ART 運(yùn)行時(shí)線程踩晶;</li><li>調(diào)用子類實(shí)現(xiàn)的成員函數(shù) FinishPhase 執(zhí)行 GC 結(jié)束階段执泰。</li></ol></li><li>并行 GC :<ol><li>調(diào)用子類實(shí)現(xiàn)的成員函數(shù) InitializePhase 執(zhí)行 GC 初始化階段;</li><li>獲取用于訪問 Java 堆的鎖渡蜻;</li><li>調(diào)用子類實(shí)現(xiàn)的成員函數(shù) MarkingPhase 執(zhí)行 GC 并行標(biāo)記階段术吝;</li><li>釋放用于訪問 Java 堆的鎖;</li><li>掛起所有的 ART 運(yùn)行時(shí)線程茸苇;</li><li>調(diào)用子類實(shí)現(xiàn)的成員函數(shù) HandleDirtyObjectsPhase 處理在 GC 并行標(biāo)記階段被修改的對(duì)象排苍;</li><li>恢復(fù)第 5 步掛起的 ART 運(yùn)行時(shí)線程;</li><li>重復(fù)第 5 到第 7 步学密,直到所有在 GC 并行階段被修改的對(duì)象都處理完成淘衙;</li><li>獲取用于訪問 Java 堆的鎖;</li><li>調(diào)用子類實(shí)現(xiàn)的成員函數(shù) ReclaimPhase 執(zhí)行 GC 回收階段腻暮;</li><li>釋放用于訪問 Java 堆的鎖彤守;</li><li>調(diào)用子類實(shí)現(xiàn)的成員函數(shù) FinishPhase 執(zhí)行 GC 結(jié)束階段。</li></ol></li></ul>它們的區(qū)別在于:<ol><li>非并行 GC 的標(biāo)記階段和回收階段是在掛住所有的 ART 運(yùn)行時(shí)線程的前提下進(jìn)行的哭靖,因此只需要執(zhí)行一次標(biāo)記即可具垫;</li><li>并行 GC 的標(biāo)記階段只鎖住了Java 堆,因此它不能阻止那些不是正在分配對(duì)象的 ART 運(yùn)行時(shí)線程同時(shí)運(yùn)行试幽,而這些同時(shí)進(jìn)運(yùn)行的 ART 運(yùn)行時(shí)線程可能會(huì)引用了一些在之前的標(biāo)記階段沒有被標(biāo)記的對(duì)象筝蚕,如果不對(duì)這些對(duì)象進(jìn)行重新標(biāo)記的話,那么就會(huì)導(dǎo)致它們被 GC 回收造成錯(cuò)誤铺坞,因此與非并行 GC 相比起宽,并行 GC 多了一個(gè)處理臟對(duì)象的階段,所謂的臟對(duì)象就是我們前面說的在 GC 標(biāo)記階段同時(shí)運(yùn)行的 ART 運(yùn)行時(shí)線程訪問或者修改過的對(duì)象康震;</li><li>并行 GC 并不是自始至終都是并行的燎含,例如處理臟對(duì)象的階段就是需要掛起除 GC 線程以外的其它 ART 運(yùn)行時(shí)線程,這樣才可以保證標(biāo)記階段可以結(jié)束腿短。</li></ol>
  上面 ART 堆內(nèi)存分配的時(shí)候屏箍,我們提到了有兩種可能會(huì)觸發(fā) GC 的情況,第一種情況是沒有足夠內(nèi)存分配給請(qǐng)求時(shí)橘忱,會(huì)調(diào)用 Heap 類的成員函數(shù) CollectGarbageInternal 觸發(fā)一個(gè)原因?yàn)?kGcCauseForAlloc 的 GC赴魁;第二種情況下分配出請(qǐng)求的內(nèi)存之后,堆剩下的內(nèi)存超過一定的閥值钝诚,就會(huì)調(diào)用 Heap 類的成員函數(shù) RequestConcurrentGC 請(qǐng)求執(zhí)行一個(gè)并行 GC颖御;此外,還有第三種情況會(huì)觸發(fā)GC,如下所示:

void Heap::CollectGarbage(bool clear_soft_references) {  
  // Even if we waited for a GC we still need to do another GC since weaks allocated during the  
  // last GC will not have necessarily been cleared.  
  Thread* self = Thread::Current();  
  WaitForConcurrentGcToComplete(self);  
  CollectGarbageInternal(collector::kGcTypeFull, kGcCauseExplicit, clear_soft_references);  
}  

當(dāng)我們調(diào)用 Java 層的 java.lang.System 的靜態(tài)成員函數(shù) gc 時(shí)潘拱,如果 ART 運(yùn)行時(shí)支持顯式 GC疹鳄,那么它就會(huì)通過 JNI 調(diào)用 Heap 類的成員函數(shù) CollectGarbageInternal 來觸發(fā)一個(gè)原因?yàn)?kGcCauseExplicit 的 GC,ART 運(yùn)行時(shí)默認(rèn)是支持顯式 GC 的芦岂,但是可以通過啟動(dòng)選項(xiàng) -XX:+DisableExplicitGC 來關(guān)閉瘪弓。所以 ART 運(yùn)行時(shí)在三種情況下會(huì)觸發(fā) GC,這三種情況通過三個(gè)枚舉 kGcCauseForAlloc禽最、kGcCauseBackground 和 kGcCauseExplicitk 來描述:

// What caused the GC?  
enum GcCause {  
  // GC triggered by a failed allocation. Thread doing allocation is blocked waiting for GC before  
  // retrying allocation.  
  kGcCauseForAlloc,  
  // A background GC trying to ensure there is free memory ahead of allocations.  
  kGcCauseBackground,  
  // An explicit System.gc() call.  
  kGcCauseExplicit,  
};  

ART 運(yùn)行時(shí)的所有 GC 都是以 Heap 類的成員函數(shù) CollectGarbageInternal 為入口:

collector::GcType Heap::CollectGarbageInternal(collector::GcType gc_type, GcCause gc_cause,  
                                               bool clear_soft_references) {  
  Thread* self = Thread::Current();  
  ......  
  
  // Ensure there is only one GC at a time.  
  bool start_collect = false;  
  while (!start_collect) {  
    {  
      MutexLock mu(self, *gc_complete_lock_);  
      if (!is_gc_running_) {  
        is_gc_running_ = true;  
        start_collect = true;  
      }  
    }  
    if (!start_collect) {  
      // TODO: timinglog this.  
      WaitForConcurrentGcToComplete(self);  
      ......  
    }  
  }  
  
  ......  
  
  if (gc_type == collector::kGcTypeSticky &&  
      alloc_space_->Size() < min_alloc_space_size_for_sticky_gc_) {  
    gc_type = collector::kGcTypePartial;  
  }  
  
  ......  
  
  collector::MarkSweep* collector = NULL;  
  for (const auto& cur_collector : mark_sweep_collectors_) {  
    if (cur_collector->IsConcurrent() == concurrent_gc_ && cur_collector->GetGcType() == gc_type) {  
      collector = cur_collector;  
      break;  
    }  
  }  
  ......  
  
  collector->clear_soft_references_ = clear_soft_references;  
  collector->Run();  
  ......  
  
  {  
      MutexLock mu(self, *gc_complete_lock_);  
      is_gc_running_ = false;  
      last_gc_type_ = gc_type;  
      // Wake anyone who may have been waiting for the GC to complete.  
      gc_complete_cond_->Broadcast(self);  
  }  
  
  ......  
  
  return gc_type;  
}  

參數(shù) gc_type 和 gc_cause 分別用來描述要執(zhí)行的 GC 的類型和原因腺怯,而參數(shù) clear_soft_references 用來描述是否要回收被軟引用指向的對(duì)象,Heap 類的成員函數(shù) CollectGarbageInternal 的執(zhí)行邏輯:<ol><li>通過一個(gè) while 循環(huán)不斷地檢查 Heap 類的成員變量 is_gc_running_川无,直到它的值等于 false 為止呛占,這表示當(dāng)前沒有其它線程正在執(zhí)行 GC,當(dāng)它的值等于 true 時(shí)就表示其它線程正在執(zhí)行 GC懦趋,這時(shí)候就要調(diào)用 Heap 類的成員函數(shù) WaitForConcurrentGcToComplete 等待其執(zhí)行完成晾虑,注意在當(dāng)前 GC 執(zhí)行之前,Heap 類的成員變量 is_gc_running_ 會(huì)被設(shè)置為true仅叫;</li><li>如果當(dāng)前請(qǐng)求執(zhí)行的 GC 類型為 kGcTypeSticky走贪,但是當(dāng)前 Allocation Space 的大小小于 Heap 類的成員變量 min_alloc_space_size_for_sticky_gc_ 指定的閥值,那么就改為執(zhí)行類型為 kGcTypePartial惑芭;</li><li>從 Heap 類的成員變量 mark_sweep_collectors_ 指向的一個(gè)垃圾收集器列表找到一個(gè)合適的垃圾收集器來執(zhí)行 GC,ART 運(yùn)行時(shí)在內(nèi)部創(chuàng)建了六個(gè)垃圾收集器继找,這六個(gè)垃圾收集器分為兩組遂跟,一組支持并行 GC,另一組不支持婴渡;每一組都是由三個(gè)類型分別為 kGcTypeSticky幻锁、kGcTypePartial 和 kGcTypeFull 的垃垃圾收集器組成,這里說的合適的垃圾收集器是指并行性與 Heap 類的成員變量 concurrent_gc_ 一致边臼,并且類型也與參數(shù) gc_type 一致的垃圾收集器哄尔;</li><li>找到合適的垃圾收集器之后,就將參數(shù) clear_soft_references 的值保存在它的成員變量 clear_soft_references_ 中柠并,以便可以告訴它要不要回收被軟引用指向的對(duì)象岭接,然后再調(diào)用它的成員函數(shù) Run 來執(zhí)行 GC;</li><li>GC 執(zhí)行完畢臼予,將 Heap 類的成員變量 is_gc_running_ 設(shè)置為false鸣戴,以表示當(dāng)前 GC 已經(jīng)執(zhí)行完畢,下一次請(qǐng)求的 GC 可以執(zhí)行了粘拾,此外也會(huì)將 Heap 類的成員變量 last_gc_type_ 設(shè)置為當(dāng)前執(zhí)行的 GC 的類型窄锅,這樣下一次執(zhí)行 GC 時(shí),就可以執(zhí)行另外一個(gè)不同類型的 GC缰雇,例如如果上一次執(zhí)行的 GC 的類型為 kGcTypeSticky入偷,那么接下來的兩次 GC 的類型就可以設(shè)置為 kGcTypePartial 和 kGcTypeFull追驴,這樣可以使得每次都能執(zhí)行有效的 GC;</li><li>通過 Heap 類的成員變量 gc_complete_cond_ 喚醒那些正在等待 GC 執(zhí)行完成的線程疏之。</li></ol>

ART GC 與 Dalvik GC 對(duì)比

?比起 Dalvik 的回收策略殿雪,ART 的 CMS(concurrent mark sweep,同步標(biāo)記回收)有以下幾個(gè)優(yōu)點(diǎn):<ul><li>阻塞的次數(shù)相比于 Dalvik 來說体捏,從兩次減少到了一次冠摄,Dalvik 第一次的阻塞大部分工作是在標(biāo)記 root,而在 ART CMS 中則是被每個(gè)執(zhí)行線程同步標(biāo)記它們自己的 root 完成的几缭,所以 ART 能夠立馬繼續(xù)運(yùn)行河泳;</li><li>和 Dalvik 類似,ART GC 同樣在回收?qǐng)?zhí)行之前有一次暫停年栓,但是關(guān)鍵的不同是 Dalvik 的一些執(zhí)行階段在 ART 中是并行同步執(zhí)行的拆挥,這些階段包括標(biāo)記過程、系統(tǒng) weak Reference 清理過程(比如 jni weak globals 等)某抓、重新標(biāo)記非 GC Root 節(jié)點(diǎn)纸兔,Card 區(qū)域的提前清理。在 ART 中仍然需要阻塞的過程是掃描 Card 區(qū)域的臟數(shù)據(jù)和重新標(biāo)記 GC Root否副,這兩個(gè)操作能夠降低阻塞的時(shí)間汉矿;</li><li>還有一個(gè) ART GC 比 Dalvik 有提升的地方是 sticky CMS 提升了 GC 的吞吐量,不像正常的分代 GC 機(jī)制备禀,sticky CMS 是不移動(dòng)堆內(nèi)存的洲拇,它不會(huì)給新對(duì)象分配一個(gè)特定的區(qū)域(年輕代),新分配的對(duì)象被保存在一個(gè)分配棧里面曲尸,這個(gè)棧就是一個(gè)簡(jiǎn)單的 Object 數(shù)組赋续,這就避免了所需要移動(dòng)對(duì)象的操作,也就獲得了低阻塞性另患,但是缺點(diǎn)就是會(huì)增加堆的對(duì)象復(fù)雜性纽乱;</li></ul>
  如果應(yīng)用程序在前臺(tái)運(yùn)行時(shí),這時(shí)候 GC 被稱為 Foreground GC昆箕,同時(shí) ART 還有一個(gè) Background GC鸦列,顧名思義就是在后臺(tái)運(yùn)行的 GC,應(yīng)用程序在前臺(tái)運(yùn)行時(shí)響應(yīng)性是最重要的鹏倘,因此也要求執(zhí)行的 GC 是高效的敛熬,相反應(yīng)用程序在后臺(tái)運(yùn)行時(shí),響應(yīng)性不是最重要的第股,這時(shí)候就適合用來解決堆的內(nèi)存碎片問題应民,因此上面提到的所有 Mark-Sweep GC 適合作為 Foreground GC,而 Compacting GC(壓縮 GC) 適合作為 Background GC。當(dāng)從 Foreground GC 切換到 Background GC诲锹,或者從 Background GC 切換到 Foreground GC繁仁,ActivityManager 會(huì)通知發(fā)生一次 Compacting GC 的行為,這是由于 Foreground GC 和 Background GC 的底層堆空間結(jié)構(gòu)是一樣的归园,因此發(fā)生 Foreground GC 和 Background GC 切換時(shí)黄虱,需要將當(dāng)前存活的對(duì)象從一個(gè) Space 轉(zhuǎn)移到另外一個(gè) Space 上去,這個(gè)剛好就是 Semi-Space compaction 和 Homogeneous space compaction 適合干的事情庸诱。Background GC 壓縮內(nèi)存就能夠使得內(nèi)存碎片變少捻浦,從而達(dá)到縮減內(nèi)存的目的,但是壓縮內(nèi)存的時(shí)候會(huì)暫時(shí)阻塞應(yīng)用進(jìn)程桥爽。 Semi-Space compaction 和 Homogeneous space compaction 有一個(gè)共同特點(diǎn)是都具有一個(gè) From Space 和一個(gè) To Space朱灿,在 GC 執(zhí)行期間,在 From Space 分配的還存活的對(duì)象會(huì)被依次拷貝到 To Space 中钠四,這樣就可以達(dá)到消除內(nèi)存碎片的目的盗扒。與 Semi-Space compaction 相比,Homogeneous space compaction 還多了一個(gè) Promote Space缀去,當(dāng)一個(gè)對(duì)象是在上一次 GC 之前分配的侣灶,并且在當(dāng)前 GC 中仍然是存活的,那么它就會(huì)被拷貝到 Promote Space 而不是 To Space 中缕碎,這相當(dāng)于是簡(jiǎn)單地將對(duì)象劃分為新生代和老生代的褥影,即在上一次 GC 之前分配的對(duì)象屬于老生代的,而在上一次 GC 之后分配的對(duì)象屬于新生代的咏雌,一般來說老生代對(duì)象的存活性要比新生代的久伪阶,因此將它們拷貝到 Promote Space 中去,可以避免每次執(zhí)行 Semi-Space compaction 或者 Homogeneous space compaction 時(shí)都需要對(duì)它們進(jìn)行無用的處理处嫌,我們來看看這兩種 Background GC 的執(zhí)行過程圖:
<center>

Semi-Space compaction
</center>
<center>Semi-Space compaction</center>
<center>
這里寫圖片描述
</center>
<center>Homogeneous space compaction</center>
以上圖片來自:ART運(yùn)行時(shí)Semi-Space(SS)和Generational Semi-Space(GSS)GC執(zhí)行過程分析,Bump Pointer Space 1 和 Bump Pointer Space 2 就是我們前面說的 From Space 和 To Space斟湃。Semi-Space compaction 一般發(fā)生在低內(nèi)存的設(shè)備上熏迹,而 Homogenous space compaction 是非低內(nèi)存設(shè)備上的默認(rèn)壓縮模式。

GC Roots 解析

?GC Roots 特指的是垃圾收集器(Garbage Collector)的對(duì)象凝赛,GC 會(huì)收集那些不是 GC Roots 且沒有被 GC Roots 引用的對(duì)象注暗,一個(gè)對(duì)象可以屬于多個(gè) Root,GC Roots 有幾下種:<ul><li> Class </li>由系統(tǒng)類加載器(system class loader)加載的對(duì)象墓猎,這些類是不能夠被回收的捆昏,他們可以以靜態(tài)字段的方式持有其它對(duì)象。我們需要注意的一點(diǎn)就是毙沾,通過用戶自定義的類加載器加載的類骗卜,除非相應(yīng)的 java.lang.Class 實(shí)例以其它的某種(或多種)方式成為 Roots,否則它們并不是 Roots;<li> Thread </li>活著的線程寇仓;<li>Stack Local</li>Java 方法的 local 變量或參數(shù)举户;<li>JNI Local</li>JNI 方法的 local 變量或參數(shù);<li>JNI Global</li>全局 JNI 引用遍烦;<li>Monitor Used</li>用于同步的監(jiān)控對(duì)象俭嘁;<li>Held by JVM</li>用于 JVM 特殊目的由 GC 保留的對(duì)象,但實(shí)際上這個(gè)與 JVM 的實(shí)現(xiàn)是有關(guān)的服猪,可能已知的一些類型是系統(tǒng)類加載器供填、一些 JVM 熟悉的重要異常類、一些用于處理異常的預(yù)分配對(duì)象以及一些自定義的類加載器等罢猪,然而 JVM 并沒有為這些對(duì)象提供其它的信息近她,因此就只有留給分析分員去確定哪些是屬于 "JVM 持有" 的了。</ul>來源:https://www.yourkit.com/docs/java/help/gc_roots.jsp

ART 日志分析

?ART 的 log 不同于 Dalvik 的 log 機(jī)制坡脐,不是明確調(diào)用的情況下不會(huì)打印的 GCs 的 log 信息泄私,GC只會(huì)在被判定為很慢時(shí)輸出信息,更準(zhǔn)確地說就是 GC 暫停的時(shí)間超過 5ms 或者 GC 執(zhí)行的總時(shí)間超過 100ms备闲。如果 app 不是處于一種停頓可察覺的狀態(tài)晌端,那么 GC 就不會(huì)被判定為執(zhí)行緩慢,但是此時(shí)顯式 GC 信息會(huì)被 log 出來恬砂,參考自:Investigating Your RAM Usage咧纠。

I/art: <GC_Reason> <GC_Name> <Objects_freed>(<Size_freed>) AllocSpace Objects, <Large_objects_freed>(<Large_object_size_freed>) <Heap_stats> LOS objects, <Pause_time(s)>

例如:

I/art : Explicit concurrent mark sweep GC freed 104710(7MB) AllocSpace objects, 21(416KB) LOS objects, 33% free, 25MB/38MB, paused 1.230ms total 67.216ms

<ul>
<li>GC Reason :什么觸發(fā)了GC,以及屬于哪種類型的垃圾回收泻骤,可能出現(xiàn)的值包括<ul><li> Concurrent </li>并發(fā) GC漆羔,不會(huì)掛起 app 線程,這種 GC 在后臺(tái)線程中運(yùn)行狱掂,不會(huì)阻止內(nèi)存分配演痒;<li> Alloc </li>GC 被初始化,app 在 heap 已滿的時(shí)候請(qǐng)求分配內(nèi)存趋惨,此時(shí) GC 會(huì)在當(dāng)前線程(請(qǐng)求分配內(nèi)存的線程)執(zhí)行鸟顺;<li> Explicit </li>GC 被 app 顯式請(qǐng)求,例如通過調(diào)用 System.gc() 或者 runtime.gc()器虾,和 Dalvik 一樣讯嫂,ART 建議相信 GC,盡可能地避免顯式調(diào)用 GC兆沙,不建議顯式調(diào)用 GC 的原因是因?yàn)闀?huì)阻塞當(dāng)前線程并引起不必要的 CPU 周期欧芽,如果 GC 導(dǎo)致其它線程被搶占的話,顯式 GC 還會(huì)引發(fā) jank(jank是指第 n 幀繪制過后葛圃,本該繪制第 n+1 幀千扔,但因?yàn)?CPU 被搶占憎妙,數(shù)據(jù)沒有準(zhǔn)備好乳丰,只好再顯示一次第 n 幀卓起,下一次繪制時(shí)顯示第 n+1);<li> NativeAlloc </li>來自 native 分配的 native memory 壓力引起的 GC讹弯,比如 Bitmap 或者 RenderScript 對(duì)象洞渤;<li> CollectorTransition </li>heap 變遷引起的 GC阅嘶,運(yùn)行時(shí)動(dòng)態(tài)切換 GC 造成的,垃圾回收器變遷過程包括從 free-list backed space 復(fù)制所有對(duì)象到 bump pointer space(反之亦然)载迄,當(dāng)前垃圾回收器過渡只會(huì)在低 RAM 設(shè)備的 app 改變運(yùn)行狀態(tài)時(shí)發(fā)生讯柔,比如從可察覺的停頓態(tài)到非可察覺的停頓態(tài)(反之亦然);<li> HomogeneousSpaceCompact </li>HomogeneousSpaceCompact 指的是 free-list space 空間的壓縮护昧,經(jīng)常在 app 變成不可察覺的停頓態(tài)時(shí)發(fā)生魂迄,這樣做的主要原因是減少 RAM 占用并整理 heap 碎片;<li> DisableMovingGc </li>不是一個(gè)真正的 GC 原因惋耙,正在整理碎片的 GC 被 GetPrimitiveArrayCritical 阻塞捣炬,一般來說因?yàn)?GetPrimitiveArrayCritical 會(huì)限制垃圾回收器內(nèi)存移動(dòng),強(qiáng)烈建議不要使用绽榛;<li> HeapTrim </li>不是一個(gè)真正的 GC 原因湿酸,僅僅是一個(gè)收集器被阻塞直到堆壓縮完成的記錄。</ul></li>
<li> GC Name:ART有幾種不同的GC<ul><li>Concurrent mark sweep (CMS)</li>全堆垃圾收集器灭美,負(fù)責(zé)收集釋放除 image space(上面 ART 堆的圖片中對(duì)應(yīng)區(qū)域)外的所有空間推溃;<li>Concurrent partial mark sweep</li>差不多是全堆垃圾收集器,負(fù)責(zé)收集除 image space 和 zygote space 外的所有空間届腐;<li>Concurrent sticky mark sweep</li>分代垃圾收集器铁坎,只負(fù)責(zé)釋放從上次 GC 到現(xiàn)在分配的對(duì)象,該 GC 比全堆和部分標(biāo)記清除執(zhí)行得更頻繁犁苏,因?yàn)樗於彝nD更短硬萍;<li>Marksweep + semispace</li>非同步的,堆拷貝壓縮和 HomogeneousSpaceCompaction 同時(shí)執(zhí)行围详。</ul></li>
<li>Objects freed</li>本次 GC 從非大對(duì)象空間(non large object space)回收的對(duì)象數(shù)目朴乖。
<li>Size freed</li>本次 GC 從非大對(duì)象空間回收的字節(jié)數(shù)。
<li>Large objects freed</li>本次 GC 從大對(duì)象空間里回收的對(duì)象數(shù)目短曾。
<li>Large object size freed</li>本次GC從大對(duì)象空間里回收的字節(jié)數(shù)。
<li>Heap stats</li>可用空間所占的百分比和 [已使用內(nèi)存大小] / [ heap 總大小]赐劣。
<li>Pause times</li>一般情況下嫉拐,GC 運(yùn)行時(shí)停頓次數(shù)和被修改的對(duì)象引用數(shù)成比例,目前 ART CMS GC 只會(huì)在 GC 結(jié)束的時(shí)停頓一次魁兼,GC 過渡會(huì)有一個(gè)長(zhǎng)停頓婉徘,是 GC 時(shí)耗的主要因素漠嵌。
</ul>

Java/Android 引用解析

?GC 過程是和對(duì)象引用的類型是嚴(yán)重相關(guān)的,我們?cè)谄綍r(shí)接觸到的一般有三種引用類型盖呼,強(qiáng)引用儒鹿、軟引用、弱引用和虛引用:

級(jí)別 回收時(shí)機(jī) 用途 生存時(shí)間
強(qiáng)引用 從來不會(huì) 對(duì)象的一般狀態(tài) Cool
軟引用 在內(nèi)存不足的時(shí)候 聯(lián)合 ReferenceQueue 構(gòu)造有效期短/占內(nèi)存大/生命周期長(zhǎng)的對(duì)象的二級(jí)高速緩沖器(內(nèi)存不足時(shí)才清空) 內(nèi)存不足時(shí)終止
弱引用 在垃圾回收時(shí) 聯(lián)合 ReferenceQueue 構(gòu)造有效期短/占內(nèi)存大/生命周期長(zhǎng)的對(duì)象的一級(jí)高速緩沖器(系統(tǒng)發(fā)生GC則清空) GC 運(yùn)行后終止
虛引用 在垃圾回收時(shí) 聯(lián)合 ReferenceQueue 來跟蹤對(duì)象被垃圾回收器回收的活動(dòng) GC 運(yùn)行后終止

在 Java/Android 開發(fā)中几晤,為了防止內(nèi)存溢出约炎,在處理一些占內(nèi)存大而且生命周期比較長(zhǎng)對(duì)象的時(shí)候,可以盡量應(yīng)用軟引用和弱引用蟹瘾,軟/弱引用可以和一個(gè)引用隊(duì)列(ReferenceQueue)聯(lián)合使用圾浅,如果軟引用所引用的對(duì)象被垃圾回收器回收,Java 虛擬機(jī)就會(huì)把這個(gè)軟引用加入到與之關(guān)聯(lián)的引用隊(duì)列中憾朴,利用這個(gè)隊(duì)列可以得知被回收的軟/弱引用的對(duì)象列表狸捕,從而為緩沖器清除已失效的軟/弱引用。

Android 內(nèi)存泄漏和優(yōu)化

?具體的請(qǐng)看中篇:Android 性能優(yōu)化之內(nèi)存泄漏檢測(cè)以及內(nèi)存優(yōu)化(中)和下篇:Android 性能優(yōu)化之內(nèi)存泄漏檢測(cè)以及內(nèi)存優(yōu)化(下)众雷。

引用

http://blog.csdn.net/luoshengyang/article/details/42555483
http://blog.csdn.net/luoshengyang/article/details/41688319
http://blog.csdn.net/luoshengyang/article/details/42492621
http://blog.csdn.net/luoshengyang/article/details/41338251
http://blog.csdn.net/luoshengyang/article/details/41581063
https://mp.weixin.qq.com/s?__biz=MzA4MzEwOTkyMQ==&mid=2667377215&idx=1&sn=26e3e9ec5f4cf3e7ed1e90a0790cc071&chksm=84f32371b384aa67166a3ff60e3f8ffdfbeed17b4c8b46b538d5a3eec524c9d0bcac33951a1a&scene=0&key=c2240201df732cf062d22d3cf95164740442d817864520af90bb0e71fa51102f2e91475a4f597ec20653c59d305c8a3e518d3f575d419dfcf8fb63a776e0d9fa6d3a9a6a52e84fedf3f467fe4af1ba8b&ascene=0&uin=Mjg5MDI3NjQ2Mg%3D%3D&devicetype=iMac+MacBookPro11%2C4+OSX+OSX+10.12.3+build(16D32)&version=12010310&nettype=WIFI&fontScale=100&pass_ticket=Upl17Ws6QQsmZSia%2F%2B0xkZs9DYxAJBQicqh8rcaxYUjcu3ztlJUPxYrQKML%2BUtuf
http://geek.csdn.net/news/detail/127226
http://www.reibang.com/p/216b03c22bb8
https://zhuanlan.zhihu.com/p/25213586
https://joyrun.github.io/2016/08/08/AndroidMemoryLeak/
http://www.cnblogs.com/larack/p/6071209.html
https://source.android.com/devices/tech/dalvik/gc-debug.html
http://blog.csdn.net/high2011/article/details/53138202
http://gityuan.com/2015/10/03/Android-GC/
http://www.ayqy.net/blog/android-gc-log%E8%A7%A3%E8%AF%BB/
https://developer.android.com/studio/profile/investigate-ram.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末灸拍,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子砾省,更是在濱河造成了極大的恐慌鸡岗,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件纯蛾,死亡現(xiàn)場(chǎng)離奇詭異纤房,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)翻诉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門炮姨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人碰煌,你說我怎么就攤上這事舒岸。” “怎么了芦圾?”我有些...
    開封第一講書人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵蛾派,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我个少,道長(zhǎng)洪乍,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任夜焦,我火速辦了婚禮壳澳,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘茫经。我一直安慰自己巷波,他們只是感情好萎津,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著抹镊,像睡著了一般锉屈。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上垮耳,一...
    開封第一講書人閱讀 51,624評(píng)論 1 305
  • 那天颈渊,我揣著相機(jī)與錄音,去河邊找鬼氨菇。 笑死儡炼,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的查蓉。 我是一名探鬼主播乌询,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼豌研!你這毒婦竟也來了妹田?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤鹃共,失蹤者是張志新(化名)和其女友劉穎鬼佣,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體霜浴,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡晶衷,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了阴孟。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片晌纫。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖永丝,靈堂內(nèi)的尸體忽然破棺而出锹漱,到底是詐尸還是另有隱情,我是刑警寧澤慕嚷,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布哥牍,位于F島的核電站,受9級(jí)特大地震影響喝检,放射性物質(zhì)發(fā)生泄漏嗅辣。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一挠说、第九天 我趴在偏房一處隱蔽的房頂上張望澡谭。 院中可真熱鬧,春花似錦纺涤、人聲如沸译暂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽外永。三九已至,卻和暖如春拧咳,著一層夾襖步出監(jiān)牢的瞬間伯顶,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工骆膝, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留祭衩,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓阅签,卻偏偏與公主長(zhǎng)得像掐暮,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子政钟,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容