Android內(nèi)存管理及內(nèi)存泄漏分析(一)

1、堆和棧

要了解Android的內(nèi)存,必須先從Java的堆和??雌?,我們先看看《Think In Java》中對(duì)它們的定義:

(1)堆棧(stack):位于通用RAM中,但通過它的“堆棧指針”可以從處理器哪里獲得支持。堆棧指針若向下移動(dòng),則分配新的內(nèi)存;若向上移動(dòng),則釋放那些內(nèi)存。這是一種快速有效的分配存儲(chǔ)方法,僅次于寄存器。創(chuàng)建程序時(shí)候,JAVA編譯器必須知道存儲(chǔ)在堆棧內(nèi)所有數(shù)據(jù)的確切大小和生命周期,因?yàn)樗仨毶上鄳?yīng)的代碼,以便上下移動(dòng)堆棧指針。這一約束限制了程序的靈活性,所以雖然某些JAVA數(shù)據(jù)存儲(chǔ)在堆棧中——特別是對(duì)象引用,但是JAVA對(duì)象不存儲(chǔ)其中。

(2) 堆(heap):一種通用性的內(nèi)存池(也存在于RAM中),用于存放所有的JAVA對(duì)象。堆不同于堆棧的好處是:編譯器不需要知道要從堆里分配多少存儲(chǔ)區(qū)域,也不必知道存儲(chǔ)的數(shù)據(jù)在堆里存活多長(zhǎng)時(shí)間。因此,在堆里分配存儲(chǔ)有很大的靈活性。當(dāng)你需要?jiǎng)?chuàng)建一個(gè)對(duì)象的時(shí)候,只需要new寫一行簡(jiǎn)單的代碼,當(dāng)執(zhí)行這行代碼時(shí),會(huì)自動(dòng)在堆里進(jìn)行存儲(chǔ)分配。當(dāng)然,為這種靈活性必須要付出相應(yīng)的代碼。用堆進(jìn)行存儲(chǔ)分配比用堆棧進(jìn)行存儲(chǔ)存儲(chǔ)需要更多的時(shí)間。

簡(jiǎn)單總結(jié)就是,棧中存放著局部變量的引用及方法的引用,堆中放著具體的對(duì)象。這里需要注意,棧中只放的是變量的引用,而不是變量指向的對(duì)象,有一咱例外情況是,現(xiàn)在有一些優(yōu)化的JVM,在判斷到某些局部變量創(chuàng)建的對(duì)象在方法退出或代碼塊退出后就可以回收時(shí),為了加快速度,會(huì)把這個(gè)對(duì)象的內(nèi)存分配在棧內(nèi)存中,這種JVM目前很少,可以暫時(shí)不用考慮。

JVM中還有另外幾塊內(nèi)存區(qū),我們也簡(jiǎn)單介紹一下:

(3)寄存器(register)。這是最快的存儲(chǔ)區(qū),因?yàn)樗挥诓煌谄渌鎯?chǔ)區(qū)的地方——處理器內(nèi)部。但是寄存器的數(shù)量極其有限,所以寄存器由編譯器根據(jù)需求進(jìn)行分配。你不能直接控制,也不能在程序中感覺到寄存器存在的任何跡象。

(4)靜態(tài)存儲(chǔ)(static storage)。這里的“靜態(tài)”是指“在固定的位置”(盡管也位于RAM)。靜態(tài)存儲(chǔ)里存放程序運(yùn)行時(shí)一直存在的數(shù)據(jù)。你可用關(guān)鍵字static來標(biāo)識(shí)一個(gè)對(duì)象的特定元素是靜態(tài)的,但JAVA對(duì)象本身從來不會(huì)存放在靜態(tài)存儲(chǔ)空間里。

(5)常量存儲(chǔ)(constant storage)。常量值通常直接存放在程序代碼內(nèi)部,這樣做是安全的,因?yàn)樗鼈冇肋h(yuǎn)不會(huì)被改變。有時(shí),在嵌入式系統(tǒng)中,常量本身會(huì)和其他部分分割離開,所以在這種情況下,可以選擇將其放在ROM中。(這點(diǎn)的一個(gè)例子便是字符串池,所有字面字符串和字符串常量表達(dá)式都被放到一個(gè)特殊的靜態(tài)存儲(chǔ)空間里。)

下面這個(gè)圖看起來會(huì)更加直觀一些:

運(yùn)行時(shí)內(nèi)存區(qū)域展示

上面這個(gè)圖,我們可以發(fā)現(xiàn),多出一個(gè)“本地方法?!保僖粋€(gè)“常量存儲(chǔ)區(qū)”,這里需要解釋一下,“本地方法?!币矊儆跅?,不過主要用于JNI Native層的變量?jī)?nèi)存,這里就不細(xì)講它了;“常量存儲(chǔ)區(qū)”實(shí)際上在包含在方法區(qū)中的,嚴(yán)格意義上來說,它不算獨(dú)立的內(nèi)存區(qū),但它里面存放的內(nèi)容又和類定義不同,所以就單提出來了。

關(guān)于Java層的棧內(nèi)容,這里需要獨(dú)立說一下,JVM中所謂的棧,指的是線程棧,即每一個(gè)線程都有一個(gè)自己的棧內(nèi)存區(qū),如下圖所示:

棧內(nèi)存展示

實(shí)例分析:

我們看一下下面這段代碼的內(nèi)存分配情況

public? class? AppMain { //運(yùn)行時(shí), jvm 把a(bǔ)ppmain的信息都放入方法區(qū)

public? static? void? main(String[] args) { //main 方法本身放入方法區(qū)。

Sample test1 = new? Sample( " 測(cè)試1 " );? //test1是引用,所以放到棧區(qū)里, Sample是自定義對(duì)象應(yīng)該放到堆里面

Sample test2 = new? Sample( " 測(cè)試2 " );

test1.printName();

test2.printName();

}

}

public class Sample { ? //運(yùn)行時(shí), jvm 把a(bǔ)ppmain的信息都放入方法區(qū)

private String name;

/** 構(gòu)造方法 */

public? Sample(String name) {

this .name = name;

}

/** 輸出 */

public void printName() {? //print方法本身放入 方法區(qū)里。

System.out.println(name);

}

}

內(nèi)存分配情況圖

這里延伸出來一個(gè)平時(shí)Java經(jīng)典的面試題:

String aa = new String("aa"); // 這句代碼生成了幾個(gè)對(duì)象?

2、Java內(nèi)存自動(dòng)回收機(jī)制

? ? JVM的代碼是開源的,Sun公司并沒有對(duì)JVM的垃圾回收機(jī)制做規(guī)范性的要求,同時(shí),JVM對(duì)內(nèi)存垃圾回收提供了很多參數(shù),所以,各家公司在實(shí)現(xiàn)自己JVM時(shí),大都不一樣,特別是三星的手機(jī),他們的JVM一直比較特殊,在垃圾回收上也表現(xiàn)的跟常規(guī)的JVM不一樣。

垃圾回收的兩個(gè)關(guān)鍵點(diǎn)是:查找垃圾、清理垃圾,這兩個(gè)關(guān)鍵點(diǎn)都有很多不同的技術(shù),我們分別來介紹一下。

先介紹一下垃圾查找,常見的有兩種查找方法:

(1)引用計(jì)數(shù)法

堆中的每一個(gè)對(duì)象有一個(gè)引用計(jì)數(shù),當(dāng)一個(gè)對(duì)象被創(chuàng)建,并把指向該對(duì)象的引用賦值給一個(gè)變量時(shí),引用計(jì)數(shù)置為1,當(dāng)再把這個(gè)引用賦值給其他變量時(shí),引用計(jì)數(shù)加1,當(dāng)一個(gè)對(duì)象的引用超過了生命周期或者被設(shè)置為新值時(shí),對(duì)象的引用計(jì)數(shù)減1,任何引用計(jì)數(shù)為0的對(duì)象都可以被當(dāng)成垃圾回收。當(dāng)一個(gè)對(duì)象被回收時(shí),它所引用的任何對(duì)象計(jì)數(shù)減1,這樣,可能會(huì)導(dǎo)致其他對(duì)象也被當(dāng)垃圾回收。

問題:很難檢測(cè)出對(duì)象之間的額相互引用(引用循環(huán)問題)

這種方式理解起來簡(jiǎn)單直接,執(zhí)行起來效率也很高,但缺點(diǎn)也十分明顯,我們可能會(huì)疑問,還有人用這種方式嗎,有Objective-C中在用,它的循環(huán)引用交給開發(fā)者去處理了。

(2)可達(dá)性分析算法(根搜索算法)

此算法的基本思想就是選取一系列GCRoots對(duì)象作為起點(diǎn),開始向下遍歷搜索其他相關(guān)的對(duì)象,搜索所走過的路徑成為引用鏈,遍歷完成后,如果一個(gè)對(duì)象到GCRoots對(duì)象沒有任何引用鏈,則證明此對(duì)象是不可用的,可以被當(dāng)做垃圾進(jìn)行回收。那么問題又來了,如何選取GCRoots對(duì)象呢?在Java語言中,可以作為GCRoots的對(duì)象包括下面幾種:

虛擬機(jī)棧(棧幀中的局部變量區(qū),也叫做局部變量表)中引用的對(duì)象。

方法區(qū)中的類靜態(tài)屬性引用的對(duì)象。

方法區(qū)中常量引用的對(duì)象。

本地方法棧中JNI(Native方法)引用的對(duì)象。

根搜索法

Java中采用的是根搜索法來判斷對(duì)象是不是可回收,這里需要注意,Roots結(jié)點(diǎn)是可以有多個(gè)的,另一方面即使一個(gè)對(duì)象被確認(rèn)為可回收的對(duì)象,像Object 6,也不是說這個(gè)對(duì)象肯定會(huì)被回收,這就牽扯到finalize()方法的調(diào)用了。

對(duì)于可達(dá)性分析算法而言,未到達(dá)的對(duì)象并非是“非死不可”的,若要宣判一個(gè)對(duì)象死亡,至少需要經(jīng)歷兩次標(biāo)記階段。1. 如果對(duì)象在進(jìn)行可達(dá)性分析后發(fā)現(xiàn)沒有與GCRoots相連的引用鏈,則該對(duì)象被第一次標(biāo)記并進(jìn)行一次篩選,篩選條件為是否有必要執(zhí)行該對(duì)象的finalize方法,若對(duì)象沒有覆蓋finalize方法或者該finalize方法是否已經(jīng)被虛擬機(jī)執(zhí)行過了,則均視作不必要執(zhí)行該對(duì)象的finalize方法,即該對(duì)象將會(huì)被回收。反之,若對(duì)象覆蓋了finalize方法并且該finalize方法并沒有被執(zhí)行過,那么,這個(gè)對(duì)象會(huì)被放置在一個(gè)叫F-Queue的隊(duì)列中,之后會(huì)由虛擬機(jī)自動(dòng)建立的、優(yōu)先級(jí)低的Finalizer線程去執(zhí)行,而虛擬機(jī)不必要等待該線程執(zhí)行結(jié)束,即虛擬機(jī)只負(fù)責(zé)建立線程,其他的事情交給此線程去處理。2.對(duì)F-Queue中對(duì)象進(jìn)行第二次標(biāo)記,如果對(duì)象在finalize方法中拯救了自己,即關(guān)聯(lián)上了GCRoots引用鏈,如把this關(guān)鍵字賦值給其他變量,那么在第二次標(biāo)記的時(shí)候該對(duì)象將從“即將回收”的集合中移除,如果對(duì)象還是沒有拯救自己,那就會(huì)被回收。如下代碼演示了一個(gè)對(duì)象如何在finalize方法中拯救了自己,然而,它只能拯救自己一次,第二次就被回收了。

finallize方法只能被執(zhí)行一次。

我們?cè)賮斫榻B一下垃圾清理的方法,一般來說,清理的方法包含三種,也都比較容易理解:

(1)標(biāo)記-清除算法

首先標(biāo)記出所有需要回收的對(duì)象,使用可達(dá)性分析算法判斷一個(gè)對(duì)象是否為可回收,在標(biāo)記完成后統(tǒng)一回收所有被標(biāo)記的對(duì)象。下圖是算法具體的一次執(zhí)行過程后的結(jié)果對(duì)比:

標(biāo)記清楚法

說明:1.效率問題,標(biāo)記和清除兩個(gè)階段的效率都不高。2.空間問題,標(biāo)記清除后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,以后需要給大對(duì)象分配內(nèi)存時(shí),會(huì)提前觸發(fā)一次垃圾回收動(dòng)作。

(2)復(fù)制算法

將內(nèi)存分為兩等塊,每次使用其中一塊。當(dāng)這一塊內(nèi)存用完后,就將還存活的對(duì)象復(fù)制到另外一個(gè)塊上面,然后再把已經(jīng)使用過的內(nèi)存空間一次清理掉。下圖是算法具體的一次執(zhí)行過程后的結(jié)果對(duì)比:

復(fù)制算法

說明:1.無內(nèi)存碎片問題。2.可用內(nèi)存縮小為原來的一半。 3.當(dāng)存活的對(duì)象數(shù)量很多時(shí),復(fù)制的效率很慢。

(3)標(biāo)記-整理算法

標(biāo)記過程還是和標(biāo)記 - 清除算法一樣,之后讓所有存活的對(duì)象都向一端移動(dòng),然后直接清理掉邊界以外的內(nèi)存,標(biāo)記 - 整理算法示意圖如下:

標(biāo)記整理法

說明:無碎片

在進(jìn)行GC時(shí),為了防止誤判斷垃圾或回收錯(cuò)誤,一般要是暫停其它線程執(zhí)行的,這樣會(huì)造成程序的卡頓,而現(xiàn)在硬件的配置越來越高,常規(guī)方法回收一次垃圾占用的時(shí)間會(huì)很大,所以對(duì)于如何執(zhí)行GC,即GC的執(zhí)行時(shí)機(jī)和方法也創(chuàng)新出很多不同的方法:

(1)Serial收集器

Serial收集器為單線程收集器,在進(jìn)行垃圾收集時(shí),必須要暫停其他所有的工作線程,直到它收集結(jié)束。運(yùn)行過程如下圖所示

說明:1. 需要STW(Stop The World),停頓時(shí)間長(zhǎng)。2. 簡(jiǎn)單高效,對(duì)于單個(gè)CPU環(huán)境而言,Serial收集器由于沒有線程交互開銷,可以獲取最高的單線程收集效率。

(2)ParNew收集器(Parallel New)

ParNew是Serial的多線程版本,除了使用多線程進(jìn)行垃圾收集外,其他行為與Serial完全一樣,運(yùn)行過程如下圖所示

說明:1.Server模式下虛擬機(jī)的首選新生收集器,與CMS進(jìn)行搭配使用。

(3)Parallel Old收集器

老年代的多線程收集器,使用標(biāo)記 - 整理算法,吞吐量?jī)?yōu)先,適合于Parallel Scavenge搭配使用,運(yùn)行過程如下圖所示

(4)CMS收集器

CMS(Conrrurent Mark Sweep)收集器是以獲取最短回收停頓時(shí)間為目標(biāo)的收集器。使用標(biāo)記 - 清除算法,收集過程分為如下四步:

初始標(biāo)記,標(biāo)記GCRoots能直接關(guān)聯(lián)到的對(duì)象,時(shí)間很短。

并發(fā)標(biāo)記,進(jìn)行GCRoots Tracing(可達(dá)性分析)過程,時(shí)間很長(zhǎng)。

重新標(biāo)記,修正并發(fā)標(biāo)記期間因用戶程序繼續(xù)運(yùn)作而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那一部分對(duì)象的標(biāo)記記錄,時(shí)間較長(zhǎng)。

并發(fā)清除,回收內(nèi)存空間,時(shí)間很長(zhǎng)。

其中,并發(fā)標(biāo)記與并發(fā)清除兩個(gè)階段耗時(shí)最長(zhǎng),但是可以與用戶線程并發(fā)執(zhí)行。運(yùn)行過程如下圖所示:

說明:無法處理浮動(dòng)垃圾,因?yàn)樵诓l(fā)清理階段用戶線程還在運(yùn)行,自然就會(huì)產(chǎn)生新的垃圾,而在此次收集中無法收集他們,只能留到下次收集,這部分垃圾為浮動(dòng)垃圾,同時(shí),由于用戶線程并發(fā)執(zhí)行,所以需要預(yù)留一部分老年代空間提供并發(fā)收集時(shí)程序運(yùn)行使用。

還有其它的一些收集方式,像G1,Serial Old等,各種方法都是在盡力減少因?yàn)镚C造成的運(yùn)行停頓。

這一節(jié)最后,我們?cè)僖幌鲁R姷膬?nèi)存收回時(shí)的日志

GC_FOR_MALLOC: 表示是在堆上分配對(duì)象時(shí)內(nèi)存不足觸發(fā)的GC。

GC_CONCURRENT: 當(dāng)我們應(yīng)用程序的堆內(nèi)存達(dá)到一定量,或者可以理解為快要滿的時(shí)候,系統(tǒng)會(huì)自動(dòng)觸發(fā)GC操作來釋放內(nèi)存。

GC_EXPLICIT: 表示是應(yīng)用程序調(diào)用System.gc、VMRuntime.gc接口或者收到SIGUSR1信號(hào)時(shí)觸發(fā)的GC。

GC_BEFORE_OOM: 表示是在準(zhǔn)備拋OOM異常之前進(jìn)行的最后努力而觸發(fā)的GC。

3、Android內(nèi)存管理

Android操作系統(tǒng),本質(zhì)上還是一個(gè)精簡(jiǎn)的linux操作系統(tǒng),我們平常使用的各個(gè)APP,盡管數(shù)量龐大,且最為我們熟知,但實(shí)際上它只是運(yùn)行在linux系統(tǒng)上的一個(gè)虛擬機(jī)進(jìn)程而已,所以我們看Android的內(nèi)存,會(huì)從宏觀和微觀兩個(gè)方面來分析,一方面看操作系統(tǒng)的整體內(nèi)存管理方式,另一方面,看看Dalvik虛擬機(jī)內(nèi)部的內(nèi)存管理:

(1)匿名共享內(nèi)存ashmem(Anonymous Shared Memory)

Linux系統(tǒng)有自己的內(nèi)存共享機(jī)制mmap。

mmap系統(tǒng)調(diào)用是將一個(gè)打開的文件映射到進(jìn)程的用戶空間,mmap系統(tǒng)調(diào)用使得進(jìn)程之間通過映射同一個(gè)普通文件實(shí)現(xiàn)共享內(nèi)存。普通文件被映射到進(jìn)程地址空間后,進(jìn)程可以像訪問普通內(nèi)存一樣對(duì)文件進(jìn)行訪問,不必再調(diào)用read(),write()等操作。

mmap 函數(shù)原型:

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

mmap的本質(zhì)是進(jìn)程間無法訪問彼此之間的內(nèi)存空間,但通過fd可以操作同一個(gè)文件,所以mmap就把一個(gè)文件(或者是設(shè)備)影射到內(nèi)存,把這個(gè)內(nèi)存地址給不同時(shí)的進(jìn)程去操作,從而實(shí)現(xiàn)共享。當(dāng)然,它的共享沒我們想象的那么low,要不停的寫磁盤,它是在內(nèi)存中完成的,減少多次內(nèi)存copy。

mmap的一個(gè)很大的缺限是共享內(nèi)存申請(qǐng)后不可變,這樣導(dǎo)致申請(qǐng)多了浪費(fèi),申請(qǐng)少了又可能不夠,Android就擴(kuò)展了mmap,在它的基礎(chǔ)上實(shí)現(xiàn)了匿名共享內(nèi)存ashmem:

ashmem為進(jìn)程間提供大塊共享內(nèi)存,同時(shí)為內(nèi)核提供回收和管理這個(gè)內(nèi)存的機(jī)制。

相比于malloc和anonymous/namedmmap等傳統(tǒng)的內(nèi)存分配機(jī)制,其優(yōu)勢(shì)是通過內(nèi)核驅(qū)動(dòng)提供了輔助內(nèi)核的內(nèi)存回收算法機(jī)制(pin/unpin)。什么是pin和unpin呢?具體來講,就是當(dāng)你使用Ashmem分配了一塊內(nèi)存,但是其中某些部分卻不會(huì)被使用時(shí),那么就可以將這塊內(nèi)存unpin掉。

unpin后,內(nèi)核可以將它對(duì)應(yīng)的物理頁面回收,以作他用。你也不用擔(dān)心進(jìn)程無法對(duì)unpin掉的內(nèi)存進(jìn)行再次訪問,因?yàn)榛厥蘸蟮膬?nèi)存還可以再次被獲得(通過缺頁handler),因?yàn)閡npin操作并不會(huì)改變已經(jīng) mmap的地址空間。

這個(gè)pin和unpin,在我們的Bitmap中,就是我們熟悉的BitmapFactory.Options里面的isPurgeable屬性,這個(gè)可以去研究一下skia的源碼,其實(shí)isPurgeable設(shè)置為true,也不能保證圖片的內(nèi)存一定分配在共享內(nèi)容中,skia只對(duì)大圖才會(huì)用ashmem存儲(chǔ)。

從pin和unpin我們就可以看出,ashmem的一個(gè)最大的好處就是內(nèi)存利用率, 申請(qǐng)到一塊兒大內(nèi)存后,再分成小塊,不用的可以隨時(shí)回收復(fù)用,對(duì)于手機(jī),特別是早年硬件配置低的手機(jī),這是非常有用的。Android上大名鼎鼎的Binder使用的就是ashmem實(shí)現(xiàn)進(jìn)程間通信的。

asheme的源碼位置:kernel/mm/ashmem.c

asheme代碼分析可以見這個(gè)文章 :Ashmem 對(duì) Android 內(nèi)存分配與共享的增強(qiáng)

Android中還用到了pmem,它的作用與ashmem相同,不過它只能申請(qǐng)連續(xù)內(nèi)存,且有最大申請(qǐng)次數(shù)限制,所以它主要用在一些特殊設(shè)備接口上,比如:DSP、GPU等。

(2)dalvik內(nèi)存

對(duì)于dalvik內(nèi)存,我們首先需要明確一點(diǎn),一個(gè)dalvik就是一個(gè)進(jìn)程,正常的app都至少對(duì)應(yīng)一個(gè)dalvik(多進(jìn)程的app在執(zhí)行到相關(guān)程序時(shí),會(huì)起多個(gè)),所以,大家的手機(jī)上平時(shí)都是多個(gè)dalvik同時(shí)處于運(yùn)行狀態(tài)。dalvik與進(jìn)程對(duì)應(yīng)起來后,每個(gè)dalvik使用的內(nèi)存資源對(duì)于操作系統(tǒng)而言,就是對(duì)進(jìn)程內(nèi)存的管理,各進(jìn)程之間內(nèi)存資源相互獨(dú)立,都有自己的內(nèi)存相對(duì)地址空間。

Dalvik VM和JVM 在內(nèi)存上的主要區(qū)別是 Dalvik VM是基于寄存器的架構(gòu)(reg based),而JVM是棧機(jī)(stack based)。reg based VM的好處是可以做到更好的提前優(yōu)化(ahead-of-time optimization)。 另外reg based的VM執(zhí)行起來更快,但是代價(jià)是更大的代碼長(zhǎng)度。

簡(jiǎn)單一句話總結(jié)就是:Dalvik效率高,移植難;JVM跨平臺(tái)容易,效率稍遜。

Android現(xiàn)在采用的ART運(yùn)行模式是比dalvik更先進(jìn)的VM,我們這是里就不分析ART了。

我們看一下JVM啟動(dòng)時(shí)的常見參數(shù)設(shè)置:

java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0

-Xms 表示初始堆大小

-Xmx 表示最大堆大小

-Xss 表示每個(gè)線程的堆棧大小

對(duì)于Dalvik,每個(gè)手機(jī)也都有類似的啟動(dòng)參數(shù),一般可以在文件/system/build.prop中查看:

dalvik.vm.heapstartsize=8m

dalvik.vm.heapgrowthlimit=192m

dalvik.vm.heapsize=512m

dalvik.vm.heapstartsize=8m 相當(dāng)于虛擬機(jī)的 -Xms配置,該項(xiàng)用來設(shè)置堆內(nèi)存的初始大小。

dalvik.vm.heapgrowthlimit=192m 相當(dāng)于虛擬機(jī)的 -XX:HeapGrowthLimit配置,該項(xiàng)用來設(shè)置一個(gè)標(biāo)準(zhǔn)的應(yīng)用的最大堆內(nèi)存大小。一個(gè)標(biāo)準(zhǔn)的應(yīng)用就是沒有使用android:largeHeap的應(yīng)用。

dalvik.vm.heapsize=512m 相當(dāng)于虛擬機(jī)的 -Xmx配置,該項(xiàng)設(shè)置了使用android:largeHeap的應(yīng)用的最大堆內(nèi)存大小。

所以,我們?cè)趯慉ndroid程序時(shí),對(duì)于內(nèi)存分配的大小,特別是一些緩存,像圖片緩存,byte[]緩存,其它對(duì)象緩存等,都要考慮這些值,而不是根據(jù)Android版本號(hào)或者屏幕大小去統(tǒng)一設(shè)置一個(gè)緩存大小值?。

獲取Dalvik堆內(nèi)存大小的幾種方法:

// 1、通過ActivityManager獲取

ActivityManager activityManager=(ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE);

activityManager.getMemoryClass();

activityManager.getLargeMemoryClass();

// 2、通過Runtime獲取

Runtime rt = Runtime.getRuntime();

rt.getFreeMemory();

rt.getMaxMemory();

rt.getTotalMemroy();

// 注意:ActivityManager.getMemroyInfo(),這個(gè)方法不是用來獲取dalvik內(nèi)存的,這是獲取系統(tǒng)總內(nèi)存的,我們?cè)O(shè)置緩存大小時(shí),一般不以它有依據(jù);

問題:一個(gè)dalvik占用的內(nèi)存是很大的,如果手機(jī)上同時(shí)運(yùn)行著幾十個(gè)dalvik,手機(jī)內(nèi)存受的了嗎?我們接著往下看。

(3)VSS RSS PSS USS

在看這幾個(gè)內(nèi)存值之前,我看需要先了解一下linux中的動(dòng)態(tài)鏈接庫(kù)的內(nèi)存分配,所謂動(dòng)態(tài)鏈接庫(kù),就是我們平時(shí)編出來的so文件,linux為了提供動(dòng)態(tài)鏈接庫(kù)的效率,使用共享內(nèi)存來加載so,它使用的內(nèi)存是mmap,這部分內(nèi)存可以被各進(jìn)程共享。所以,在Android中,so的庫(kù)內(nèi)存是只有一份的,但要注意,so中的代碼新申請(qǐng)的內(nèi)存是屬于進(jìn)程的,并不在共享內(nèi)存中。

這也就解釋了上面的問題,dalvik中的很多的so都是共享的,所以有很大一部分是共享內(nèi)存,真正在dalvik進(jìn)程的內(nèi)存沒有那么多;盡管如此,如果一個(gè)進(jìn)程只是想執(zhí)行一些與Android組件無關(guān)的代碼(像文件處理),最好直接寫一個(gè)可執(zhí)行的文件,而不是在dalvik中執(zhí)行。

我們來看看關(guān)于這四個(gè)內(nèi)存指標(biāo)的解釋:

VSS:Virtual Set Size,虛擬耗用內(nèi)存。它是一個(gè)進(jìn)程能訪問的所有內(nèi)存空間地址的大小。這個(gè)大小包含了一些沒有駐留在RAM中的內(nèi)存,就像mallocs已經(jīng)被分配,但還沒有寫入。VSS很少用來測(cè)量程序的實(shí)際使用內(nèi)存。

RSS:Resident Set Size,實(shí)際使用物理內(nèi)存。RSS是一個(gè)進(jìn)程在RAM中實(shí)際持有的內(nèi)存大小。RSS可能會(huì)產(chǎn)生誤導(dǎo),因?yàn)樗怂性撨M(jìn)程使用的共享庫(kù)所占用的內(nèi)存,一個(gè)被加載到內(nèi)存中的共享庫(kù)可能有很多進(jìn)程會(huì)使用它。RSS不是單個(gè)進(jìn)程使用內(nèi)存量的精確表示。

PSS:Proportional Set Size,實(shí)際使用的物理內(nèi)存,它與RSS不同,它會(huì)按比例分配共享庫(kù)所占用的內(nèi)存。

例如,如果有三個(gè)進(jìn)程共享一個(gè)占30頁內(nèi)存控件的共享庫(kù),每個(gè)進(jìn)程在計(jì)算PSS的時(shí)候,只會(huì)計(jì)算10頁。

PSS是一個(gè)非常有用的數(shù)值,如果系統(tǒng)中所有的進(jìn)程的PSS相加,所得和即為系統(tǒng)占用內(nèi)存的總和。當(dāng)一個(gè)進(jìn)程被殺死后,它所占用的共享庫(kù)內(nèi)存將會(huì)被其他仍然使用該共享庫(kù)的進(jìn)程所分擔(dān)。在這種方式下,PSS也會(huì)帶來誤導(dǎo),因?yàn)楫?dāng)一個(gè)進(jìn)程被殺后,PSS并不代表系統(tǒng)回收的內(nèi)存大小。

USS:Unique Set Size,進(jìn)程獨(dú)自占用的物理內(nèi)存。這部分內(nèi)存完全是該進(jìn)程獨(dú)享的。USS是一個(gè)非常有用的數(shù)值,因?yàn)樗砻髁诉\(yùn)行一個(gè)特定進(jìn)程所需的真正內(nèi)存成本。當(dāng)一個(gè)進(jìn)程被殺死,USS就是所有系統(tǒng)回收的內(nèi)存。USS是用來檢查進(jìn)程中是否有內(nèi)存泄露的最好選擇。

以上這些都是官方解釋的中文譯本。

查看這幾個(gè)指標(biāo)的方法:

top? | grep app名稱

ps? |? grep app名稱

procrank | grep app名稱

dumpsys meminfo app名稱

前兩個(gè)命令只能查到VSS RSS內(nèi)存占用信息。

而后面兩個(gè)命令可以查出 ?PSS USS內(nèi)存占用。

top

dumpsys meminfo

我們平時(shí)遇到的OOM中的heapSize,一般指的是uss,并不包括共享內(nèi)存部分。

(4)LowMemoryKiller

Android是一個(gè)多任務(wù)系統(tǒng),也就是說可以同時(shí)運(yùn)行多個(gè)程序,這個(gè)大家應(yīng)該很熟悉。一般來說,啟動(dòng)運(yùn)行一個(gè)程序是有一定的時(shí)間開銷的,因此為了加快運(yùn)行速度,當(dāng)你退出一個(gè)程序時(shí),Android并不會(huì)立即殺掉它,這樣下次再運(yùn)行該程序時(shí),可以很快的啟動(dòng)。隨著系統(tǒng)中保留的程序越來越多,內(nèi)存肯定會(huì)出現(xiàn)不足,這個(gè)時(shí)候Android系統(tǒng)開始揮舞屠刀殺程序。這里就有一個(gè)很明顯的問題,殺誰?

Android系統(tǒng)中殺程序的這個(gè)劊子手被稱作"LowMemory Killer",它是在Linux內(nèi)核中實(shí)現(xiàn)的。這里它實(shí)現(xiàn)了一個(gè)機(jī)制,由程序的重要性來決定殺誰。通俗來說,誰不干活,先殺誰。

一:Android六大進(jìn)程:

Android將程序的重要性分成以下幾類,按照重要性依次降低的順序:

1.前臺(tái)進(jìn)程(foreground):

目前正在屏幕上顯示的進(jìn)程和一些系統(tǒng)進(jìn)程。舉例來說,當(dāng)你運(yùn)行一個(gè)程序,如瀏覽器,當(dāng)瀏覽器界面在前臺(tái)顯示時(shí),瀏覽器屬于前臺(tái)進(jìn)程(foreground),但一旦你按home回到主界面,瀏覽器就變成了后臺(tái)程序(background)。我們最不希望終止的進(jìn)程就是前臺(tái)進(jìn)程。

2.可見進(jìn)程(visible):

可見進(jìn)程是一些不再前臺(tái),但用戶依然可見的進(jìn)程,舉個(gè)例來說:widget、輸入法等,都屬于visible。這部分進(jìn)程雖然不在前臺(tái),但與我們的使用也密切相關(guān),我們也不希望它們被終止(你肯定不希望時(shí)鐘、天氣,新聞等widget被終止,那它們將無法同步,你也不希望輸入法被終止,否則你每次輸入時(shí)都需要重新啟動(dòng)輸入法)

3.次要服務(wù)(secondary server):

目前正在運(yùn)行的一些服務(wù)(主要服務(wù),如撥號(hào)等,是不可能被進(jìn)程管理終止的,故這里只談次要服務(wù))簡(jiǎn)單來說就是一些殺掉了不影響系統(tǒng)穩(wěn)定運(yùn)行,但是嚴(yán)重影響用戶使用的服務(wù)。如GMS(GoogleMobile Service),即谷歌移動(dòng)服務(wù)、撥號(hào)器等,殺掉相當(dāng)影響用戶使用。

4.后臺(tái)進(jìn)程(hidden):

雖然作者用了hidden這個(gè)詞,但實(shí)際即是后臺(tái)進(jìn)程(background),就是我們通常意義上理解的啟動(dòng)后被切換到后臺(tái)的進(jìn)程,如瀏覽器,閱讀器等。當(dāng)程序顯示在屏幕上時(shí),他所運(yùn)行的進(jìn)程即為前臺(tái)進(jìn)程(foreground),一旦我們按home返回主界面(注意是按home,不是按back),程序就駐留在后臺(tái),成為后臺(tái)進(jìn)程

5.內(nèi)容供應(yīng)節(jié)點(diǎn)(content provider):

沒有程序?qū)嶓w,進(jìn)提供內(nèi)容供別的程序去用的,比如日歷供應(yīng)節(jié)點(diǎn),郵件供應(yīng)節(jié)點(diǎn)等。在終止進(jìn)程時(shí),這類程序應(yīng)該有較高的優(yōu)先權(quán)(對(duì)于用戶來說看不到只是代碼里有這個(gè))

6.空進(jìn)程(empty):

沒有任何東西在內(nèi)運(yùn)行的進(jìn)程,有些程序,在程序退出后,依然會(huì)在進(jìn)程中駐留一個(gè)空進(jìn)程,這個(gè)進(jìn)程里沒有任何數(shù)據(jù)在運(yùn)行,作用往往是提高該程序下次的啟動(dòng)速度或者記錄程序的一些歷史信息。這部分進(jìn)程無疑是應(yīng)該最先終止的。

二:oom_adj和oom_score

Low Memorry Killer的機(jī)制主要是通過進(jìn)程的oom_adj和oom_score來進(jìn)行內(nèi)存的處理的

? 1. 每一個(gè)進(jìn)程都有一個(gè)oom_adj值,取值范圍[-17,15]。

? 2. 每一個(gè)進(jìn)程都有一個(gè)oom_score值,它是根據(jù)oom_adj計(jì)算出一個(gè)值,分?jǐn)?shù)越大越容易被殺死。

? 3. 內(nèi)存緊張時(shí),LMK基于oom_adj和oom_score值來決定是否要回收一個(gè)進(jìn)程。

? 4. oom_adj值越小,越不容易被殺死,其中,-17時(shí) oom_score為0表示不會(huì)被殺死。

? 5. 查看oom_adj和oom_score方法:

cat proc/pid/oom_adj

cat proc/pid/oom_score

? 六大進(jìn)程分別對(duì)應(yīng)的oom_adj值:

oom_adj的值?

在這個(gè)表中,前面代表的是程序重要性的名稱,后面的數(shù)字代表的com_adj的數(shù)值分配,當(dāng)然了,越小的值代表程序越重要,被Kill的可能性也就更小。

三:LMK的進(jìn)程回收策略

Low Memory Killer Driver在用戶空間指定了一組內(nèi)存臨界值及與之一一對(duì)應(yīng)的一組oom_adj值,當(dāng)系統(tǒng)剩余內(nèi)存位于內(nèi)存臨界值中的一個(gè)范圍內(nèi)時(shí),如果一個(gè)進(jìn)程的oom_adj值大于或等于這個(gè)臨界值對(duì)應(yīng)的oom_adj值就會(huì)進(jìn)入被殺掉隊(duì)列。(不同手機(jī)閥值不一樣) 以華為榮耀4為例:

killer 閾值

這里其實(shí)算出來的是一個(gè)閾值,閾值的意思是當(dāng)手機(jī)內(nèi)存小于閾值的情況下,內(nèi)存就會(huì)開始逐級(jí)回收該類型的內(nèi)存了。閾值中數(shù)值的單位是內(nèi)存中的頁面數(shù)量,一般情況下一個(gè)頁面是4KB。比如說15 級(jí)別是 30720 * 4K = 123 M,即當(dāng)手機(jī)內(nèi)存小于123M的時(shí)候開始回收15級(jí)別的應(yīng)用的內(nèi)存,即選擇一個(gè)oom_adj值最大并且消耗內(nèi)存最多的進(jìn)程來回收。

對(duì)于oom_score的計(jì)算方法:

oom_score計(jì)算

LMK的機(jī)制各廠商會(huì)有一些自己的優(yōu)化,像華為的OS就對(duì)殺進(jìn)程級(jí)別就修改比較多。同時(shí)oom_abj也不一定只有上面6個(gè)值 ,所以經(jīng)常會(huì)遇到我們使用官方的優(yōu)先級(jí),但最終還是會(huì)被系統(tǒng)殺死,這也是為什么我們做進(jìn)程?;顣r(shí),都是多種方法并用,以應(yīng)對(duì)不同廠商的LMK機(jī)制。

adj配置文件的位置:/sys/module/lowmemorykiller/parameters/

這里大家可以思考一個(gè)問題,也是面試中常見的一個(gè)問題:IntentService和Thread的區(qū)別是什么?

(5)dalvik Memory回收機(jī)制

dalvik的內(nèi)存回收機(jī)制,實(shí)際上跟目前主流的JVM內(nèi)存回收機(jī)制類似,都是采用分代回收的方式。我們先看看分代的內(nèi)存分配:

這里所說的內(nèi)存分配,主要指的是在堆上的分配,一般的,對(duì)象的內(nèi)存分配都是在堆上進(jìn)行,但現(xiàn)代技術(shù)也支持將對(duì)象拆成標(biāo)量類型(標(biāo)量類型即原子類型,表示單個(gè)值,可以是基本類型或String等),然后在棧上分配,在棧上分配的很少見,我們這里不考慮。

Java內(nèi)存分配和回收的機(jī)制概括的說,就是:分代分配,分代回收。對(duì)象將根據(jù)存活的時(shí)間被分為:年輕代(Young Generation)、年老代(Old Generation)、永久代(Permanent Generation,也就是方法區(qū))。如下圖

內(nèi)存分配過程

年輕代(Young Generation):對(duì)象被創(chuàng)建時(shí),內(nèi)存的分配首先發(fā)生在年輕代(大對(duì)象可以直接被創(chuàng)建在年老代),大部分的對(duì)象在創(chuàng)建后很快就不再使用,因此很快變得不可達(dá),于是被年輕代的GC機(jī)制清理掉(IBM的研究表明,98%的對(duì)象都是很快消亡的),這個(gè)GC機(jī)制被稱為Minor GC或叫Young GC。注意,Minor GC并不代表年輕代內(nèi)存不足,它事實(shí)上只表示在Eden區(qū)上的GC。

年輕代上的內(nèi)存分配是這樣的,年輕代可以分為3個(gè)區(qū)域:Eden區(qū)(伊甸園,亞當(dāng)和夏娃偷吃禁果生娃娃的地方,用來表示內(nèi)存首次分配的區(qū)域,再貼切不過)和兩個(gè)存活區(qū)(Survivor 0 、Survivor 1)。內(nèi)存分配過程為

一次gc的內(nèi)存變化過程

絕大多數(shù)剛創(chuàng)建的對(duì)象會(huì)被分配在Eden區(qū),其中的大多數(shù)對(duì)象很快就會(huì)消亡。Eden區(qū)是連續(xù)的內(nèi)存空間,因此在其上分配內(nèi)存極快;

<1>最初一次,當(dāng)Eden區(qū)滿的時(shí)候,執(zhí)行Minor GC,將消亡的對(duì)象清理掉,并將剩余的對(duì)象復(fù)制到一個(gè)存活區(qū)Survivor0(此時(shí),Survivor1是空白的,兩個(gè)Survivor總有一個(gè)是空白的);

<2>下次Eden區(qū)滿了,再執(zhí)行一次Minor GC,將消亡的對(duì)象清理掉,將存活的對(duì)象復(fù)制到Survivor1中,然后清空Eden區(qū);

<3>將Survivor0中消亡的對(duì)象清理掉,將其中可以晉級(jí)的對(duì)象晉級(jí)到Old區(qū),將存活的對(duì)象也復(fù)制到Survivor1區(qū),然后清空Survivor0區(qū);

<4>當(dāng)兩個(gè)存活區(qū)切換了幾次(HotSpot虛擬機(jī)默認(rèn)15次,用-XX:MaxTenuringThreshold控制,大于該值進(jìn)入老年代,但這只是個(gè)最大值,并不代表一定是這個(gè)值)之后,仍然存活的對(duì)象(其實(shí)只有一小部分,比如,我們自己定義的對(duì)象),將被復(fù)制到老年代。

從上面的過程可以看出,Eden區(qū)是連續(xù)的空間,且Survivor總有一個(gè)為空。經(jīng)過一次GC和復(fù)制,一個(gè)Survivor中保存著當(dāng)前還活著的對(duì)象,而Eden區(qū)和另一個(gè)Survivor區(qū)的內(nèi)容都不再需要了,可以直接清空,到下一次GC時(shí),兩個(gè)Survivor的角色再互換。因此,這種方式分配內(nèi)存和清理內(nèi)存的效率都極高,這種垃圾回收的方式就是著名的“停止-復(fù)制(Stop-and-copy)”清理法(將Eden區(qū)和一個(gè)Survivor中仍然存活的對(duì)象拷貝到另一個(gè)Survivor中),這不代表著停止復(fù)制清理法很高效,其實(shí),它也只在這種情況下高效,如果在老年代采用停止復(fù)制,則挺悲劇的。

在Eden區(qū),HotSpot虛擬機(jī)使用了兩種技術(shù)來加快內(nèi)存分配。分別是bump-the-pointer和TLAB(Thread-Local Allocation Buffers),這兩種技術(shù)的做法分別是:由于Eden區(qū)是連續(xù)的,因此bump-the-pointer技術(shù)的核心就是跟蹤最后創(chuàng)建的一個(gè)對(duì)象,在對(duì)象創(chuàng)建時(shí),只需要檢查最后一個(gè)對(duì)象后面是否有足夠的內(nèi)存即可,從而大大加快內(nèi)存分配速度;而對(duì)于TLAB技術(shù)是對(duì)于多線程而言的,將Eden區(qū)分為若干段,每個(gè)線程使用獨(dú)立的一段,避免相互影響。TLAB結(jié)合bump-the-pointer技術(shù),將保證每個(gè)線程都使用Eden區(qū)的一段,并快速的分配內(nèi)存。

年老代(Old Generation):對(duì)象如果在年輕代存活了足夠長(zhǎng)的時(shí)間而沒有被清理掉(即在幾次Young GC后存活了下來),則會(huì)被復(fù)制到年老代,年老代的空間一般比年輕代大,能存放更多的對(duì)象,在年老代上發(fā)生的GC次數(shù)也比年輕代少。當(dāng)年老代內(nèi)存不足時(shí),將執(zhí)行Major GC,也叫 Full GC。

可以使用-XX:+UseAdaptiveSizePolicy開關(guān)來控制是否采用動(dòng)態(tài)控制策略,如果動(dòng)態(tài)控制,則動(dòng)態(tài)調(diào)整Java堆中各個(gè)區(qū)域的大小以及進(jìn)入老年代的年齡。

如果對(duì)象比較大(比如長(zhǎng)字符串或大數(shù)組),Young空間不足,則大對(duì)象會(huì)直接分配到老年代上(大對(duì)象可能觸發(fā)提前GC,應(yīng)少用,更應(yīng)避免使用短命的大對(duì)象)。用-XX:PretenureSizeThreshold來控制直接升入老年代的對(duì)象大小,大于這個(gè)值的對(duì)象會(huì)直接分配在老年代上。

可能存在年老代對(duì)象引用新生代對(duì)象的情況,如果需要執(zhí)行Young GC,則可能需要查詢整個(gè)老年代以確定是否可以清理回收,這顯然是低效的。解決的方法是,年老代中維護(hù)一個(gè)512 byte的塊——”card table“,所有老年代對(duì)象引用新生代對(duì)象的記錄都記錄在這里。Young GC時(shí),只要查這里即可,不用再去查全部老年代,因此性能大大提高。

關(guān)于新生代和老生代一般占用內(nèi)存的大小分配規(guī)則:

堆大小 = 新生代 + 老年代。其中,堆的大小可以通過參數(shù) –Xms、-Xmx 來指定。

默認(rèn)的,新生代 ( Young ) 與老年代 ( Old ) 的比例的值為 1:2 ( 該值可以通過參數(shù) –XX:NewRatio 來指定 ),即:新生代 ( Young ) = 1/3 的堆空間大小。老年代 ( Old ) = 2/3 的堆空間大小。其中,新生代 ( Young ) 被細(xì)分為 Eden 和 兩個(gè) Survivor 區(qū)域,這兩個(gè) Survivor 區(qū)域分別被命名為 from 和 to,以示區(qū)分。

默認(rèn)的,Edem : from : to = 8 : 1 : 1 ( 可以通過參數(shù) –XX:SurvivorRatio 來設(shè)定 ),即: Eden = 8/10 的新生代空間大小,from = to = 1/10 的新生代空間大小。

JVM 每次只會(huì)使用 Eden 和其中的一塊 Survivor 區(qū)域來為對(duì)象服務(wù),所以無論什么時(shí)候,總是有一塊 Survivor 區(qū)域是空閑著的。

因此,新生代實(shí)際可用的內(nèi)存空間為 9/10 ( 即90% )的新生代空間。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容