操作系統對內存的管理
沒有內存抽象的年代
在早些的操作系統中,并沒有引入內存抽象的概念。程序直接訪問和操作的都是物理內存。比如當執行如下指令時:mov reg1,1000
這條指令會毫無想象力的將物理地址1000中的內容賦值給寄存器。不難想象,這種內存操作方式使得操作系統中存在多進程變得完全不可能,比如MS-DOS,你必須執行完一條指令后才能接著執行下一條。如果是多進程的話,由于直接操作物理內存地址,當一個進程給內存地址1000賦值后,另一個進程也同樣給內存地址賦值,那么第二個進程對內存的賦值會覆蓋第一個進程所賦的值,這回造成兩條進程同時崩潰。
沒有內存抽象對于內存的管理通常非常簡單,除去操作系統所用的內存之外,全部給用戶程序使用。或是在內存中多留一片區域給驅動程序使用
無內存抽象存在的問題
用戶程序可以訪問任意內存,容易破壞操作系統,造成崩潰
同時運行多個程序特別困難
內存抽象:地址空間
基址寄存器與界限寄存器可以簡單的動態重定位,每個內存地址送到內存之前,都會自動加上基址寄存器的內容。
交換技術把一個進程完全調入內存,使該進程運行一段時間,然后把它存回磁盤。空閑進程主要存在磁盤上,所以當他們不運行時就不會占用內存。
為什么要有地址空間?
首先直接把物理地址暴露給進程會帶來嚴重問題
如果用戶程序可以尋址內存的每個字節,就有很大的可能破壞操作系統,造成系統崩潰
同時運行多個程序十分困難 地址空間創造了一個新的內存抽象,地址空間是一個進程可用于尋址內存的一套地址的集合。每個進程都有一個自己的地址空間,并且這個地址空間獨立于其它進程的地址空間。使用基址寄存器和界限器可以實現。
虛擬內存
虛擬內存是現代操作系統普遍使用的一種技術。前面所講的抽象滿足了多進程的要求,但很多情況下,現有內存無法滿足僅僅一個大進程的內存要求(比如很多游戲,都是10G+的級別)。在早期的操作系統曾使用覆蓋(overlays)來解決這個問題,將一個程序分為多個塊,基本思想是先將塊0加入內存,塊0執行完后,將塊1加入內存。依次往復,這個解決方案最大的問題是需要程序員去程序進行分塊,這是一個費時費力讓人痛苦不堪的過程。后來這個解決方案的修正版就是虛擬內存。
虛擬內存的基本思想是,每個進程有用獨立的邏輯地址空間,內存被分為大小相等的多個塊,稱為頁(Page).每個頁都是一段連續的地址。對于進程來看,邏輯上貌似有很多內存空間,其中一部分對應物理內存上的一塊(稱為頁框,通常頁和頁框大小相等),還有一些沒加載在內存中的對應在硬盤上。
由上圖可以看出,虛擬內存實際上可以比物理內存大。當訪問虛擬內存時,會通過MMU(內存管理單元)去匹配對應的物理地址,而如果虛擬內存的頁并不存在于物理內存中,會產生缺頁中斷,從磁盤中取得缺的頁放入內存,如果內存已滿,還會根據某種算法將磁盤中的頁換出。
而虛擬內存和物理內存的匹配是通過頁表實現,頁表存在MMU中,頁表中每個項通常為32位,既4byte,除了存儲虛擬地址和頁框地址之外,還會存儲一些標志位,比如是否缺頁,是否修改過,寫保護等。可以把MMU想象成一個接收虛擬地址項返回物理地址的方法。
因為頁表中每個條目是4字節,現在的32位操作系統虛擬地址空間會是2的32次方,即使每頁分為4K,也需要2的20次方 * 4字節 = 4M的空間,為每個進程建立一個4M的頁表并不明智。因此在頁表的概念上進行推廣,產生二級頁表,二級頁表每個對應4M的虛擬地址,而一級頁表去索引這些二級頁表,因此32位的系統需要1024個二級頁表,雖然頁表條目沒有減少,但內存中可以僅僅存放需要使用的二級頁表和一級頁表,大大減少了內存的使用。
引入多級頁表的原因是避免把全部頁表一直存在內存中。
虛擬地址和物理地址匹配規則
虛擬頁號可用做頁表的索引,以找到該虛擬頁面對應頁表項。由頁表項可以找到頁框號。然后把頁框號拼接到偏移量的高位端,以替換虛擬頁號,形成送往內存的物理地址。
頁表的目的是把虛擬頁面映射為頁框,從數學的角度來說,頁表是一個函數,它的參數是,虛擬頁號,結果是物理頁框號。通過這個函數可以把虛擬地址中的虛擬頁面域替換為頁框域,從而形成物理地址。
頁面置換算法
地址映射過程中,若在頁面中發現所要訪問的頁面不再內存中,則產生缺頁中斷。當發生缺頁中斷時操作系統必須在內存選擇一個頁面將其移出內存,以便為即將調入的頁面讓出空間。如果要換出的頁面在內存駐留期間已經被修改過,就必須把它寫回磁盤以更新該頁面在磁盤的副本;如果該頁面沒有被修改過,那么它在磁盤上的副本已經是最新的,不需要回寫。直接用調入的頁面覆蓋掉被淘汰的頁面就可以了。而用來選擇淘汰哪一頁的規則叫做頁面置換算法。
因為在計算機系統中,讀取少量數據硬盤通常需要幾毫秒,而內存中僅僅需要幾納秒。一條CPU指令也通常是幾納秒,如果在執行CPU指令時,產生幾次缺頁中斷,那性能可想而知,因此盡量減少從硬盤的讀取無疑是大大的提升了性能。而前面知道,物理內存是極其有限的,當虛擬內存所求的頁不在物理內存中時,將需要將物理內存中的頁替換出去,選擇哪些頁替換出去就顯得尤為重要,如果算法不好將未來需要使用的頁替換出去,則以后使用時還需要替換進來,這無疑是降低效率的,讓我們來看幾種頁面替換算法。
最優頁面置換算法(Optimal Page Replacement Algorithm)
最饑餓頁面置換算法是將未來最久不使用的頁替換出去,這聽起來很簡單,但是無法實現。根據頁面被訪問前所需要的指令數作為標記,根據指令數的由多到少進行置換,這個方法對評價頁面置換算法很有用,但它在實際系統中卻不能使用,因為無法真正的實現。這種算法可以作為衡量其它算法的基準。
最近最少使用頁面置換算法(Least Recently Used)
通常在前幾條指令中使用頻繁的頁面很可能在后面幾條指令中頁頻繁使用。LRU算法就是在缺頁發生時首先置換最長時間未被使用的頁面。優秀但是難以實現。
最近未使用頁面置換算法(Not Recently Used Replacement Algorithm)
在最近的一個時鐘周期內,淘汰一個沒有被訪問的已修改頁面,近似 LRU 算法,NRU 只是更粗略些。
這種算法給每個頁一個標志位,R表示最近被訪問過,M表示被修改過。定期對R進行清零。這個算法的思路是首先淘汰那些未被訪問過R=0的頁,其次是被訪問過R=1,未被修改過M=0的頁,最后是R=1,M=1的頁。
先進先出的頁面置換算法(First-In First-Out Page Replacement Algorithm)
這種算法的思想是淘汰在內存中最久的頁,這種算法的性能接近于隨機淘汰。可能拋棄重要的頁面,并不好。
第二次機會頁面置換算法(Second Chance Page Replacement Algorithm)
這種算法是在FIFO的基礎上,為了避免置換出經常使用的頁,增加一個標志位R,如果最近使用過將R置1,當頁將會淘汰時,如果R為1,則不淘汰頁,將R置0.而那些R=0的頁將被淘汰時,直接淘汰。這種算法避免了經常被使用的頁被淘汰。
時鐘替換算法(Clock Page Replacement Algorithm)
雖然改進型FIFO算法避免置換出常用的頁,但由于需要經常移動頁,效率并不高。因此在改進型FIFO算法的基礎上,將隊列首位相連形成一個環路,當缺頁中斷產生時,從當前位置開始找R=0的頁,而所經過的R=1的頁被置0,并不需要移動頁。
下表是上面幾種算法的簡單比較:
算法 | 描述 |
---|---|
最佳置換算法 | 無法實現,最為測試基準使用 |
最近不常使用算法 | 和LRU性能差不多 |
先進先出算法 | 有可能會置換出經常使用的頁 |
改進型先進先出算法 | 和先進先出相比有很大提升 |
最久未使用算法 | 性能非常好,但實現起來比較困難 |
時鐘置換算法 | 非常實用的算法 |
分頁系統中的設計問題
在任何分頁式系統中,都需要考慮兩個主要的問題:虛擬地址到到物理地址的映射必須非常快;如果虛擬地址空間很大,頁表也會很大。
-
局部分分配策略與全局分配策略,怎樣在相互競爭的可運行進程之間分配內存.
局部分配為每個進程分配固定的內存片段,即使有大量的空閑頁框存在,工作集的增長也會顛簸。
全局分配在進程間動態地分配頁框,分配給各個進程的頁框數是動態變化的。給每個進程分配一個最小的頁框數使無論多么小的進程都可以運行,再需要更大的內存時去公共的內存池里去取。
FIFO LRU 既適用于局部算法,也適用于全局算法。WSClock 工作集更適用于局部算法。
負載控制 , 即使使用了最優的頁面置換算法,最理想的全局分配。當進程組合的工作集超出內存容量時,就可能發生顛簸。這時只能根據進程的特性(IO 密集 or CPU 密集)將進程交換到磁盤上。
頁面大小 的確定不存在全局最優的結果,小頁面減少頁面內內存浪費,但是小頁面,意味著更大的頁表,更多的計算轉換時間。現在一般的頁面大小是 4KB 或 8KB
地址空間太小,所以分離指令空間,數據空間
共享頁面,在數據空間和指令空間分離的基礎上很容易實現程序的共享,linux 采取了 copy on write 的方案,也有數據的共享。
共享庫,多個程序并發,如果有一個程序已經裝在了共享庫,其他程序就沒有必要再進行裝載,減少內存浪費。而且共享庫并不會一次性的裝入內存,而是根據需要以頁面為單位進行裝載的。共享時不使用絕對地址,使用相對偏移量的代碼(位置無關代碼 position-independent code)
共享庫是內存映射文件的一種特例,核心思想是進程可以發起一個系統調用,將一個文件映射到其虛擬地址空間的一部分,在多數實現中在映射共享的頁面時不會實際讀入頁面的內容,而是在訪問時被每次一頁的讀入,磁盤文件被當作后背存儲。當進程退出或顯示的接觸文件時,所有被改動的頁面會被寫入到文件中。
清除策略,發生缺頁中斷時有大量的空閑頁框,此時的分頁系統在最佳狀態,有一個分頁守護(paging daemon)的后臺進程,它在多數時候睡眠,但會被定期喚醒,如果空閑頁框過少,分頁守護進程通過預定的頁面置換算法選擇頁面換出內存。
虛擬內存接口,在一些高級系統中,程序員可以對內存映射進行控制,允許控制的原因是為了允許兩個或多個進程共享一部分內存。頁面共享可以用來實現高性能的消息傳遞系統。
Linux內存管理
內核空間
頁(page)是內核的內存管理的基本單位
struct page {
page_flags_t flags; 頁標志符
atomic_t _count; 頁引用計數
atomic_t _mapcount; 頁映射計數
unsigned long private; 私有數據指針
struct address_space *mapping; 該頁所在地址空間描述結構指針,用于內容為文件的頁幀
pgoff_t index; 該頁描述結構在地址空間radix樹page_tree中的對象索引號即頁號
struct list_head lru; 最近最久未使用struct slab結構指針鏈表頭變量
void *virtual; 頁虛擬地址
};
flags:頁標志包含是不是臟的,是否被鎖定等等,每一位單獨表示一種狀態,可同時表示出32種不同狀態,定義在<linux/page-flags.h>
_count:計數值為-1表示未被使用。
virtual:頁在虛擬內存中的地址,對于不能永久映射到內核空間的內存(比如高端內存),該值為NULL;需要事必須動態映射這些內存。
盡管處理器的最小可尋址單位通常為字或字節,但內存管理單元(MMU,把虛擬地址轉換為物理地址的硬件設備)通常以頁為單位處理。內核用struct page結構體表示每個物理頁,struct page結構體占40個字節,假定系統物理頁大小為4KB,對于4GB物理內存,1M個頁面,故所有的頁面page結構體共占有內存大小為40MB,相對系統4G,這個代價并不高。
內核把頁劃分在不同的區(zone)
總共3個區,具體如下:
區 | 描述 | 物理內存(MB) |
---|---|---|
ZONE_DMA | DMA使用的頁 | <16 |
ZONE_NORMAL | 可正常尋址的頁 | 16 ~896 |
ZONE_HIGHMEM | 動態映射的頁 | >896 |
執行DMA操作的內存必須從ZONE_DMA區分配
一般內存,既可從ZONE_DMA,也可從ZONE_NORMAL分配,但不能同時從兩個區分配;
用戶空間
用戶空間中進程的內存,往往稱為進程地址空間。
Linux采用虛擬內存技術。進程的內存空間只是虛擬內存(或者叫作邏輯內存),而程序的運行需要的是實實在在的內存,即物理內存(RAM)。在必要時,操作系統會將程序運行中申請的內存(虛擬內存)映射到RAM,讓進程能夠使用物理內存。
地址空間
每個進程都有一個32位或64位的地址空間,取決于體系結構。 一個進程的地址空間與另一個進程的地址空間即使有相同的內存地址,也彼此互不相干,對于這種共享地址空間的進程稱之為線程。一個進程可尋址4GB的虛擬內存(32位地址空間中),但不是所有虛擬地址都有權訪問。對于進程可訪問的地址空間稱為內存區域。每個內存區域都具有對相關進程的可讀、可寫、可執行屬性等相關權限設置。
內存區域可包含的對象:
代碼段(text section): 可執行文件代碼
數據段(data section): 可執行文件的已初始化全局變量(靜態分配的變量和全局變量)。
bss段:程序中未初始化的全局變量,零頁映射(頁面的信息全部為0值)。
進程用戶空間棧的零頁映射(進程的內核棧獨立存在并由內核維護)
每一個諸如C庫或動態連接程序等共享庫的代碼段、數據段和bss也會被載入進程的地址空間
任何內存映射文件
任何共享內存段
任何匿名的內存映射(比如由malloc()分配的內存)
這些內存區域不能相互覆蓋,每一個進程都有不同的內存片段。
內存描述符
內存描述符由mm_struct
結構體表示
struct mm_struct {
struct vm_area_struct *mmap;
rb_root_t mm_rb;
...
atomic_t mm_users;
atomic_t mm_count;
?
struct list_head mmlist;
...
};
mm_users:代表正在使用該地址的進程數目,當該值為0時mm_count也變為0;
mm_count: 代表mm_struct的主引用計數,當該值為0說明沒有任何指向該mm_struct結構體的引用,結構體會被撤銷。
-
mmap和mm_rb:描述的對象都是相同的
mmap以鏈表形式存放, 利于高效地遍歷所有元素
mm_rb以紅黑樹形式存放,適合搜索指定元素
mmlist:所有的mm_struct結構體都通過mmlist連接在一個雙向鏈表中,該鏈表的首元素是init_mm內存描述符,它代表init進程的地址空間。
在進程的進程描述符(<linux/sched.h>中定義的task_struct結構體)中,mm域記錄該進程使用的內存描述符。故current->mm代表當前進程的內存描述符。
fork()函數 利用copy_mm函數復制父進程的內存描述符,子進程中的mm_struct結構體通過allcote_mm()從高速緩存中分配得到。通常,每個進程都有唯一的mm_struct結構體,即唯一的進程地址空間。
當子進程與父進程是共享地址空間,可調用clone(),那么不再調用allcote_mm(),而是僅僅是將mm域指向父進程的mm,即 tsk->mm = current->mm。
相反地,撤銷內存是exit_mm()函數,該函數會進行常規的撤銷工作,更新一些統計量。
內核線程
沒有進程地址空間,即內核線程對應的進程描述符中mm=NULL
內核線程直接使用前一個進程的內存描述符,僅僅使用地址空間中和內核內存相關的信息
頁表
應用程序操作的對象時映射到物理內存之上的虛擬內存,而處理器直接操作的是物理內存。故應用程序訪問一個虛擬地址時,需要將虛擬地址轉換為物理地址,然后處理器才能解析地址訪問請求,這個轉換工作通過查詢頁表完成。
Linux使用三級頁表完成地址轉換。
頂級頁表:頁全局目錄(PGD),指向二級頁目錄;
二級頁表:中間頁目錄(PMD),指向PTE中的表項;
最后一級:頁表(PTE),指向物理頁面。
多數體系結構,搜索頁表工作由硬件完成。每個進程都有自己的頁表(線程會共享頁表)。為了加快搜索,實現了翻譯后緩沖器(TLB),作為將虛擬地址映射到物理地址的硬件緩存。還有寫時拷貝方式共享頁表,當fork()時,父子進程共享頁表,只有當子進程或父進程試圖修改特定頁表項時,內核才創建該頁表項的新拷貝,之后父子進程不再共享該頁表項。可見,利用共享頁表可以消除fork()操作中頁表拷貝所帶來的消耗。
進程與內存
所有進程都必須占用一定數量的內存,這些內存用來存放從磁盤載入的程序代碼,或存放來自用戶輸入的數據等。內存可以提前靜態分配和統一回收,也可以按需動態分配和回收。
對于普通進程對應的內存空間包含5種不同的數據區:
代碼段
數據段
BSS段
堆:動態分配的內存段,大小不固定,可動態擴張(malloc等函數分配內存),或動態縮減(free等函數釋放);
棧:存放臨時創建的局部變量;
進程內存空間
Linux采用虛擬內存管理技術,每個進程都有各自獨立的進程地址空間(即4G的線性虛擬空間),無法直接訪問物理內存。這樣起到保護操作系統,并且讓用戶程序可使用比實際物理內存更大的地址空間。
4G進程地址空間被劃分兩部分,內核空間和用戶空間。用戶空間從0到3G,內核空間從3G到4G;
用戶進程通常情況只能訪問用戶空間的虛擬地址,不能訪問內核空間虛擬地址。只有用戶進程進行系統調用(代表用戶進程在內核態執行)等情況可訪問到內核空間;
用戶空間對應進程,所以當進程切換,用戶空間也會跟著變化;
內核空間是由內核負責映射,不會跟著進程變化;內核空間地址有自己對應的頁表,用戶進程各自有不同額頁表。
內存分配
進程分配內存,陷入內核態分別由brk和mmap完成,但這兩種分配還沒有分配真正的物理內存,真正分配在后面會講。
-
brk: 數據段的最高地址指針_edata往高地址推
當malloc需要分配的內存<M_MMAP_THRESHOLD(默認128k)時,采用brk;
brk分配的內存需高地址內存全部釋放之后才會釋放。(由于是通過推動指針方式)
當最高地址空間的空閑內存大于M_TRIM_THRESHOLD時(默認128k),執行內存緊縮操作;
-
do_mmap:在堆棧中間的文件映射區域找空閑的虛擬內存
當malloc需要分配的內存>M_MMAP_THRESHOLD(默認128k)時,采用do_map();
mmap分配的內存可以單獨釋放
物理內存
物理內存只有進程真正去訪問虛擬地址,發生缺頁中斷時,才分配實際的物理頁面,建立物理內存和虛擬內存的映射關系。
應用程序操作的是虛擬內存;而處理器直接操作的卻是物理內存。當應用程序訪問虛擬地址,必須將虛擬地址轉化為物理地址,處理器才能解析地址訪問請求。
物理內存是通過分頁機制實現的
物理頁在系統中由也結構struct page描述,所有的page都存儲在數組mem_map[]中,可通過該數組找到系統中的每一頁。
虛擬內存 轉化為 真實物理內存:
虛擬進程空間:通過查詢進程頁表,獲取實際物理內存地址;
虛擬內核空間:通過查詢內核頁表,獲取實際物理內存地址;
物理內存映射區:物理內存映射區與實際物理去偏移量僅PAGE_OFFSET,通過通過virt_to_phys()轉化;
虛擬內存與真實物理內存映射關系:
其中物理地址空間中除了896M(ZONE_DMA + ZONE_NORMAL)的區域是絕對的物理連續,其他內存都不是物理內存連續。在虛擬內核地址空間中的安全保護區域的指針都是非法的,用于保證指針非法越界類的操作,vm_struct是連續的虛擬內核空間,對應的物理頁面可以不連續,地址范圍(3G + 896M + 8M) ~ 4G;另外在虛擬用戶空間中 vm_area_struct同樣也是一塊連續的虛擬進程空間,地址空間范圍0~3G。
碎片問題
-
外部碎片:未被分配的內存,由于太多零碎的不連續小內存,無法滿足當前較大內存的申請要求;
原因:頻繁的分配與回收物理頁導致大量的小塊內存夾雜在已分配頁面中間;
解決方案:伙伴算法有所改善
-
內部碎片:已經分配的內存,卻不能被利用的內存空間;
緣由:所有內存分配必須起始可被4、8或16(體系結構決定)整除的地址或者MMU分頁機制限制;
解決方案:slab分配器有所改善
實例:請求一個11Byte的內存塊,系統可能會分配12Byte、16Byte等稍大一些的字節,這些多余空間就產生碎片
Android進程內存管理
進程的地址空間
在32位操作系統中,進程的地址空間為0到4GB,
示意圖如下:
這里主要說明一下Stack和Heap:
Stack空間(進棧和出棧)由操作系統控制,其中主要存儲函數地址、函數參數、局部變量等等,所以Stack空間不需要很大,一般為幾MB大小。
Heap空間的使用由程序員控制,程序員可以使用malloc、new、free、delete等函數調用來操作這片地址空間。Heap為程序完成各種復雜任務提供內存空間,所以空間比較大,一般為幾百MB到幾GB。正是因為Heap空間由程序員管理,所以容易出現使用不當導致嚴重問題。
進程內存空間和RAM之間的關系
進程的內存空間只是虛擬內存(或者叫作邏輯內存),而程序的運行需要的是實實在在的內存,即物理內存(RAM)。在必要時,操作系統會將程序運行中申請的內存(虛擬內存)映射到RAM,讓進程能夠使用物理內存。
RAM作為進程運行不可或缺的資源,對系統性能和穩定性有著決定性影響。另外,RAM的一部分被操作系統留作他用,比如顯存等等,內存映射和顯存等都是由操作系統控制,我們也不必過多地關注它,進程所操作的空間都是虛擬地址空間,無法直接操作RAM。
示意圖如下:
Android中的進程
Native進程:采用C/C++實現,不包含Dalvik實例的Linux進程,/system/bin/目錄下面的程序文件運行后都是以native進程形式存在的。上圖 /system/bin/surfaceflinger、/system/bin/rild、procrank等就是Native進程。
Java進程:實例化了Dalvik虛擬機實例的Linux進程,進程的入口main函數為Java函數。Dalvik虛擬機實例的宿主進程是fork()系統調用創建的Linux進程,所以每一個Android上的Java進程實際上就是一個Linux進程,只是進程中多了一個Dalvik虛擬機實例。因此,Java進程的內存分配比Native進程復雜。下圖,Android系統中的應用程序基本都是Java進程,如桌面、電話、聯系人、狀態欄等等。
Android中進程的堆內存
進程空間中的Heap空間是我們需要重點關注的。Heap空間完全由程序員控制,我們使用的C Malloc、C++ new和Java new所申請的空間都是Heap空間, C/C++申請的內存空間在Native Heap中,而Java申請的內存空間則在Dalvik Heap中。
Android的 Java程序為什么容易出現OOM
這個是因為Android系統對Dalvik的VM Heapsize作了硬性限制,當java進程申請的Java空間超過閾值時,就會拋出OOM異常(這個閾值可以是48M、24M、16M等,視機型而定),可以通過adb shell getprop | grep dalvik.vm.heapgrowthlimit
查看此值。
也就是說,程序發生OMM并不表示RAM不足,而是因為程序申請的Java Heap對象超過了Dalvik VM Heap Growth Limit。也就是說,在RAM充足的情況下,也可能發生OOM。
這樣的設計似乎有些不合理,但是Google為什么這樣做呢?這樣設計的目的是為了讓Android系統能同時讓比較多的進程常駐內存,這樣程序啟動時就不用每次都重新加載到內存,能夠給用戶更快的響應。迫使每個應用程序使用較小的內存,移動設備非常有限的RAM就能使比較多的App常駐其中。但是有一些大型應用程序是無法忍受VM Heap Growth Limit的限制的。
Android如何應對RAM不足
Java程序發生OMM并不是表示RAM不足,如果RAM真的不足,會發生什么呢?這時Android的Memory Killer會起作用,當RAM所剩不多時,Memory Killer會殺死一些優先級比較低的進程來釋放物理內存,讓高優先級程序得到更多的內存。
應用程序如何繞過DalvikVM Heap Size的限制
對于一些大型的應用程序(比如游戲),內存使用會比較多,很容易超超出VM Heapsize的限制,這時怎么保證程序不會因為OOM而崩潰呢?
創建子進程
創建一個新的進程,那么我們就可以把一些對象分配到新進程的Heap上了,從而達到一個應用程序使用更多的內存的目的,當然,創建子進程會增加系統開銷,而且并不是所有應用程序都適合這樣做,視需求而定。
創建子進程的方法:使用android:process
標簽
使用JNI在Native Heap上申請空間(推薦使用)
Native Heap的增長并不受Dalvik VM heapsize的限制,它的Native Heap Size已經遠遠超過了Dalvik Heap Size的限制。
只要RAM有剩余空間,程序員可以一直在Native Heap上申請空間,當然如果RAM快耗盡,Memory Killer會殺進程釋放RAM。大家使用一些軟件時,有時候會閃退,就可能是軟件在Native層申請了比較多的內存導致的。比如,我就碰到過UC Web在瀏覽內容比較多的網頁時閃退,原因就是其Native Heap增長到比較大的值,占用了大量的RAM,被Memory Killer殺掉了。
使用顯存(操作系統預留RAM的一部分作為顯存)
使用OpenGL Textures等API,Texture Memory不受Dalvik VM Heapsize限制。再比如Android中的GraphicBufferAllocator申請的內存就是顯存。
Bitmap分配在Native Heap還是Dalvik Heap上?
一種流行的觀點是這樣的:
Bitmap是JNI層創建的,所以它應該是分配到Native Heap上,并且為了解釋Bitmap容易導致OOM,提出了這樣的觀點:native heap size + dalvik heapsize <= dalvik vm heapsize
。但是Native Heap Size遠遠超過Dalvik VM Heap Size,所以,事實證明以上觀點是不正確的。
正確的觀點:
大家都知道,過多地創建Bitmap會導致OOM異常,且Native Heap Size不受Dalvik限制,所以可以得出結論:
Bitmap只能是分配在Dalvik Heap上的,因為只有這樣才能解釋Bitmap容易導致OOM。
Android內存管理機制
從操作系統的角度來說,內存就是一塊數據存儲區域,是可被操作系統調度的資源。在多任務(進程)的OS中,內存管理尤為重要,OS需要為每一個進程合理的分配內存資源。所以可以從OS對內存和回收兩方面來理解內存管理機制。
- 分配機制:為每一個任務(進程)分配一個合理大小的內存塊,保證每一個進程能夠正常的運行,同時確保進程不會占用太多的內存。
- 回收機制:當系統內存不足的時候,需要有一個合理的回收再分配機制,以保證新的進程可以正常運行。回收時殺死那些正在占用內存的進程,OS需要提供一個合理的殺死進程機制。
同樣作為一個多任務的操作系統,Android系統對內存管理有有一套自己的方法,手機上的內存資源比PC更少,需要更加謹慎的管理內存。理解Android的內存分配機制有助于我們寫出更高效的代碼,提高應用的性能。
下面分別從**分配 和回收 **兩方面來描述Android的內存管理機制:
分配機制
Android為每個進程分配內存時,采用彈性的分配方式,即剛開始并不會給應用分配很多的內存,而是給每一個進程分配一個“夠用”的內存大小。這個大小值是根據每一個設備的實際的物理內存大小來決定的。隨著應用的運行和使用,Android會為進程分配一些額外的內存大小。但是分配的大小是有限度的,系統不可能為每一個應用分配無限大小的內存。
總之,Android系統需要最大限度的讓更多的進程存活在內存中,以保證用戶再次打開應用時減少應用的啟動時間,提高用戶體驗。
回收機制
Android對內存的使用方式是“盡最大限度的使用”,只有當內存水足的時候,才會殺死其它進程來回收足夠的內存。但Android系統否可能隨便的殺死一個進程,它也有一個機制殺死進程來回收內存。
Android殺死進程有兩個參考條件:
1. 回收收益
當Android系統開始殺死LRU緩存中的進程時,系統會判斷每個進程殺死后帶來的回收收益。因為Android總是傾向于殺死一個能回收更多內存的進程,從而可以殺死更少的進程,來獲取更多的內存。殺死的進程越少,對用戶體驗的影響就越小。
2. 進程優先級
下面將從 Application Framework 和 Linux kernel 兩個層次分析 Android 操作系統的資源管理機制。
Android 之所以采用特殊的資源管理機制,原因在于其設計之初就是面向移動終端,所有可用的內存僅限于系統 RAM,必須針對這種限制設計相應的優化方案。當 Android 應用程序退出時,并不清理其所占用的內存,Linux 內核進程也相應的繼續存在,所謂“退出但不關閉”。從而使得用戶調用程序時能夠在第一時間得到響應。當系統內存不足時,系統將激活內存回收過程。為了不因內存回收影響用戶體驗(如殺死當前的活動進程),Android 基于進程中運行的組件及其狀態規定了默認的五個回收優先級:
Android為每一個進程分配了優先組的概念,優先組越低的進程,被殺死的概率就越大。根據進程的重要性,劃分為5級:
1)前臺進程(Foreground process)
用戶當前操作所必需的進程。通常在任意給定時間前臺進程都為數不多。只有在內存不足以支持它們同時繼續運行這一萬不得已的情況下,系統才會終止它們。
2)可見進程(Visible process)
沒有任何前臺組件、但仍會影響用戶在屏幕上所見內容的進程。可見進程被視為是極其重要的進程,除非為了維持所有前臺進程同時運行而必須終止,否則系統不會終止這些進程。
3)服務進程(Service process)
盡管服務進程與用戶所見內容沒有直接關聯,但是它們通常在執行一些用戶關心的操作(例如,在后臺播放音樂或從網絡下載數據)。因此,除非內存不足以維持所有前臺進程和可見進程同時運行,否則系統會讓服務進程保持運行狀態。
4)后臺進程(Background process)
后臺進程對用戶體驗沒有直接影響,系統可能隨時終止它們,以回收內存供前臺進程、可見進程或服務進程使用。 通常會有很多后臺進程在運行,因此它們會保存在 LRU 列表中,以確保包含用戶最近查看的 Activity 的進程最后一個被終止。如果某個 Activity 正確實現了生命周期方法,并保存了其當前狀態,則終止其進程不會對用戶體驗產生明顯影響,因為當用戶導航回該 Activity 時,Activity 會恢復其所有可見狀態。
5)空進程(Empty process)
不含任何活動應用組件的進程。保留這種進程的的唯一目的是用作緩存,以縮短下次在其中運行組件所需的啟動時間。 為使總體系統資源在進程緩存和底層內核緩存之間保持平衡,系統往往會終止這些進程。
通常,前面三種進程不會被殺死。
ActivityManagerService 集中管理所有進程的內存資源分配。所有進程需要申請或釋放內存之前必須調用 ActivityManagerService 對象,獲得其“許可”之后才能進行下一步操作,或者 ActivityManagerService 將直接“代勞”。ActivityManagerService類中涉及到內存回收的幾個重要的成員方法如下:trimApplications()、updateOomAdjLocked()、activityIdleInternal() 。這幾個成員方法主要負責 Android 默認的內存回收機制,若 Linux 內核中的內存回收機制沒有被禁用,則跳過默認回收。
默認回收過程
Android 操作系統中的內存回收可分為兩個層次,即默認內存回收與Linux內核級內存回收,所有代碼可參見 ActivityManagerService.java。
回收動作入口:activityIdleInternal()
Android 系統中內存回收的觸發點大致可分為三種情況。第一,用戶程序調用 StartActivity(), 使當前活動的 Activity 被覆蓋;第二,用戶按 back 鍵,退出當前應用程序;第三,啟動一個新的應用程序。這些能夠觸發內存回收的事件最終調用的函數接口就是 activityIdleInternal()。當 ActivityManagerService 接收到異步消息 IDLE_TIMEOUT_MSG 或者 IDLE_NOW_MSG 時,activityIdleInternal() 將會被調用。