接上篇繼續(xù)總結(jié)內(nèi)存管理基礎(chǔ)。
五、內(nèi)存回收
無論計算機上有多少內(nèi)存都是不夠的,因而linux kernel需要通過內(nèi)存回收策略來保證系統(tǒng)持續(xù)有內(nèi)存使用。
5.1 基本概念
1. 頁分類(按有無文件背景頁面主要分兩種):
文件頁(file-backed page):有文件背景頁面。可以直接和硬盤對應(yīng)的文件進行交換。
匿名頁(anonymous page):無文件背景頁面。如進程堆、棧、數(shù)據(jù)段使用的頁等,無法直接跟磁盤交換,但是可以跟swap區(qū)進行交換。
2. 緩存:
對于有文件背景的頁面,程序去讀文件時,可以通過read也可以通過mmap去讀。通過任何一種方式從磁盤讀文件時,內(nèi)核都會給你申請一個page cache,來緩存硬盤上的內(nèi)容。這樣,讀過一遍的數(shù)據(jù)下次再讀的時候就直接從page cache里去拿,提升了性能和訪問速度。
而這里需要補充兩對概念:
1)對比兩種文件操作的方式:read/write 和 mmap
read/write: 常規(guī)文件操作為了提高讀寫效率和保護磁盤,使用了page cache機制。這樣造成讀文件時需要先將文件頁從磁盤拷貝到頁緩存中,由于頁緩存處在內(nèi)核空間,不能被用戶進程直接尋址,所以還需要將頁緩存中數(shù)據(jù)頁再次拷貝到內(nèi)存對應(yīng)的用戶空間中。這樣,通過了兩次數(shù)據(jù)拷貝過程,才能完成進程對文件內(nèi)容的獲取任務(wù)。寫操作也是一樣,待寫入的buffer在內(nèi)核空間不能直接訪問,必須要先拷貝至內(nèi)核空間對應(yīng)的主存,再寫回磁盤中(延遲寫回),也是需要兩次數(shù)據(jù)拷貝。但是讀過的數(shù)據(jù)下次再讀就直接從page cache里去拿了, 這時效率也是很高的。
mmap:創(chuàng)建新的虛擬內(nèi)存區(qū)域和建立文件磁盤地址和虛擬內(nèi)存區(qū)域映射這兩步,沒有任何文件拷貝操作。而之后訪問數(shù)據(jù)時發(fā)現(xiàn)內(nèi)存中并無數(shù)據(jù)而發(fā)起的缺頁異常過程,可以通過已經(jīng)建立好的映射關(guān)系,只使用一次數(shù)據(jù)拷貝,就從磁盤中將數(shù)據(jù)傳入內(nèi)存的用戶空間中,供進程使用。對文件的讀取操作跨過了頁緩存,減少了數(shù)據(jù)的拷貝次數(shù),用內(nèi)存讀寫取代I/O讀寫,提高了文件讀取效率。
2)cache 與 buffer
通過文件系統(tǒng)來訪問文件產(chǎn)生的緩存記錄為cache。
直接操作磁盤產(chǎn)生的緩存記錄為buffer。
這里cache提升了文件讀寫的性能和速度,但是也占用了物理內(nèi)存空間,并且在程序運行結(jié)束后,cache memory也不會自動釋放,而需要通過內(nèi)存回收策略來釋放。
5.2 哪些內(nèi)存可以回收
屬于內(nèi)核的大部分頁框是不能夠進行回收的,比如內(nèi)核棧、內(nèi)核代碼段、內(nèi)核數(shù)據(jù)段以及大部分內(nèi)核使用的頁框。
進程使用的頁框可以進行回收的,比如進程代碼段、進程數(shù)據(jù)段、進程堆棧、進程訪問文件時映射的文件頁、進程間共享內(nèi)存使用的頁。
5.3 頁回收方式
頁回寫:如果一個很少使用的頁的后備存儲器是一個塊設(shè)備(例如文件映射),則可以將內(nèi)存直接同步到塊設(shè)備,騰出的頁面可以被重用。
頁交換:如果頁面沒有后備存儲器,則可以交換到特定swap分區(qū),再次被訪問時再交換回內(nèi)存。
頁丟棄:如果頁面的后備存儲器是一個文件,但文件內(nèi)容在內(nèi)存不能被修改(例如可執(zhí)行文件),那么在當(dāng)前不需要的情況下可直接丟棄。
5.4 頁回收算法-LRU
當(dāng)Linux系統(tǒng)內(nèi)存有盈余時,內(nèi)核會盡量多地使用內(nèi)存作為page cache,提高系統(tǒng)性能,page cache會被加入到文件類型的LRU鏈表中,當(dāng)系統(tǒng)內(nèi)存緊張時,會按一定的算法來回收內(nèi)存,下面簡單了解下:
LRU鏈表按zone來配置,每個zone中都有一整套LRU鏈表。
而一個lru鏈表描述符中總共有5個雙向鏈表頭,它們分別描述五中不同類型的鏈表:
-
LRU_INACTIVE_ANON:稱為非活動匿名頁lru鏈表(swap) -
LRU_ACTIVE_ANON:稱為活動匿名頁lru鏈表(swap) -
LRU_INACTIVE_FILE:稱為非活動文件頁lru鏈表(磁盤) -
LRU_ACTIVE_FILE:稱為活動文件頁lru鏈表(磁盤) -
LRU_UNEVICTABLE:此鏈表中保存的是此zone中所有禁止換出的頁的描述符。
那么lru鏈表進行的操作主要有以下幾種:
- 將不處于lru鏈表的新頁放入到lru鏈表中
- 將非活動lru鏈表中的頁移動到非活動lru鏈表尾部
- 將處于活動lru鏈表的頁移動到非活動lru鏈表
- 將處于非活動lru鏈表的頁移動到活動lru鏈表
- 將頁從lru鏈表中移除
LRU老化規(guī)則:頁面通過lru批處理,轉(zhuǎn)來轉(zhuǎn)去,從活動鏈表轉(zhuǎn)到非活動鏈表,從非活動鏈表靠前轉(zhuǎn)到鏈尾,在內(nèi)存回收時,非活動鏈表鏈尾的頁被回收掉。
當(dāng)內(nèi)存緊張時,優(yōu)先換出無臟數(shù)據(jù)的page cache(文件頁包含page cache),直接丟棄。其次才是匿名頁和有臟數(shù)據(jù)的文件頁的回收。遵循URL老化規(guī)則。通過Swappiness來確定更傾向于回收哪種更多一點,swappiness越大,越傾向于回收匿名頁,反之越傾向于回收文件頁。將swapness=0則意味著不再交換匿名頁,swapness=100, 盡量交換匿名頁,Swappiness默認值為60。
5.4 頁回收時機
周期性回收(被動觸發(fā)):這是由后臺運行的守護進程 kswapd 完成的,回收的時機由水位控制。
直接頁面回收(主動觸發(fā)):“內(nèi)存嚴重不足”事件的觸發(fā)。
如果操作系統(tǒng)在進行了內(nèi)存回收操作之后仍然無法回收到足夠多的頁面以滿足上述內(nèi)存要求,那么操作系統(tǒng)只有最后一個選擇,那就是使用 OOM( out of memory )killer,它從系統(tǒng)中挑選一個最合適的進程殺死它,并釋放該進程所占用的所有頁面。
5.4.1 水位控制
| 名稱 | 描述 |
|---|---|
| high | 內(nèi)存回收到該值時停止回收。 |
| low | 內(nèi)存到該值時觸發(fā)kswapd線程的內(nèi)存回收。 |
| min | 如果剩余內(nèi)存減少到觸及這個水位,可認為內(nèi)存嚴重不足,當(dāng)前進程就會被堵住,kernel會直接在這個進程的進程上下文里面做直接頁面回收。 |
注:由于每個ZONE是分別管理各自內(nèi)存的,因此每個ZONE都有這三個水位。
5.4.2 回收代碼調(diào)用路徑

直接頁面回收:
系統(tǒng)會調(diào)用函數(shù) try_to_free_pages() 去檢查當(dāng)前內(nèi)存區(qū)域中的頁面,回收那些最不常用的頁面。該函數(shù)會反復(fù)調(diào)用 shrink_zones() 以及 shrink_slab() 釋放一定數(shù)目的頁面,默認值是 32 個頁面。如果在特定的循環(huán)次數(shù)內(nèi)沒有能夠成功釋放 32 個頁面,那么頁面回收會調(diào)用 OOM killer 選擇并殺死一個進程,然后釋放它占用的所有頁面。
注:OOM_killer是Linux自我保護的方式,當(dāng)內(nèi)存不足時不至于出現(xiàn)太嚴重問題,有點壯士斷腕的意味。在kernel 2.6,內(nèi)存不足將喚醒oom_killer,挑出/proc/<pid>/oom_score最大者并將之kill掉。
定期(周期性)回收:
kswapd進程以水線為觸發(fā)點,按LRU鏈表來進行回收。系統(tǒng)會調(diào)用函數(shù)balance_pgdat(),它主要調(diào)用的函數(shù)是 shrink_zone() 和 shrink_slab()。
5.4.3 函數(shù)介紹
shrink_zone()
該函數(shù)主要做了兩件事情:
1)將某些頁面從 active 鏈表移到 inactive 鏈表,這是由函數(shù) shrink_active_list() 實現(xiàn)的。
2)從 inactive 鏈表中選定一定數(shù)目的頁面,將其放到一個臨時鏈表中,這由函數(shù) shrink_inactive_list() 完成。該函數(shù)最終會調(diào)用 shrink_page_list() 去回收這些頁面。
shrink_slab()
該函數(shù)用來回收磁盤緩存所占用的頁面的。Linux 操作系統(tǒng)并不清楚這類頁面是如何使用的,所以如果希望操作系統(tǒng)回收磁盤緩存所占用的頁面,那么必須要向操作系統(tǒng)內(nèi)核注冊 shrinker 函數(shù),shrinker 函數(shù)會在內(nèi)存較少的時候主動釋放一些該磁盤緩存占用的空間。函數(shù) shrink_slab() 會遍歷 shrinker 鏈表,從而對所有注冊了 shrinker 函數(shù)的磁盤緩存進行處理。Android內(nèi)核的lowmemorykiller機制就是注冊了shrinker,內(nèi)存過低時選擇性殺死進程來回收內(nèi)存。
shrink_page_list()
邏輯流程圖:

六、用戶空間內(nèi)存管理
6.1 Android用戶空間進程劃分
-
Native進程:不包含虛擬機實例的linux進程。 -
Java進程:包含了虛擬機實例的linux進程。
6.2 內(nèi)存管理
6.2.1 Natvie進程
1)內(nèi)存區(qū)域劃分:
Native進程與Linux進程一樣,虛擬內(nèi)存區(qū)域分為:代碼區(qū)、只讀常量區(qū)、全局區(qū)、BSS段、堆區(qū)、棧區(qū)

代碼區(qū):存放函數(shù)體的二進制代碼。
只讀常量區(qū):存放字符串常量,以及const修飾的全局變量 。
全局區(qū)/數(shù)據(jù)區(qū):存放已經(jīng)初始化的全局變量和已經(jīng)初始化用static修飾的局部變量。
BSS段:存放沒有初始化的全局變量和未初始化靜態(tài)局部變量,該區(qū)域會在main函數(shù)執(zhí)行前進行自動清零。
堆區(qū):一般由程序員分配釋放,若程序員不釋放,程序結(jié)束時可能由OS回收。注意它與數(shù)據(jù)結(jié)構(gòu)中的堆是兩回事,分配方式倒是類似于鏈表。
棧區(qū):由編譯器自動分配釋放,存放函數(shù)的參數(shù)值,局部變量的值等。其操作方式類似于數(shù)據(jù)結(jié)構(gòu)中的棧。
注意:棧區(qū)和堆區(qū)之間并沒有嚴格分割線,可以進行微調(diào),并且堆區(qū)分配一般從低地址到高地址分配,而棧區(qū)分配一般從高地址到低地址分配。
以上是標準劃分,但是對于一個進程的內(nèi)存空間,邏輯上可以分為以下三部分:
程序區(qū): 程序的二進制文件。
靜態(tài)存儲區(qū):(只讀常量區(qū)、全局區(qū)、BSS段)全局變量和靜態(tài)變量。
動態(tài)存儲區(qū):(堆區(qū)、棧區(qū))本地變量。
注:
局部變量:在一個有限的范圍內(nèi)的變量,作用域是有限的,對于程序來說,在一個函數(shù)體內(nèi)部聲明的普通變量都是局部變量,局部變量會在棧上申請空間,函數(shù)結(jié)束后,申請的空間會自動釋放。
全局變量:在函數(shù)體外申請的,會被存放在全局(靜態(tài)區(qū))上,知道程序結(jié)束后才會被結(jié)束,這樣它的作用域就是整個程序。
靜態(tài)變量:和全局變量的存儲方式相同,在函數(shù)體內(nèi)聲明為static就可以使此變量像全局變量一樣使用,不用擔(dān)心函數(shù)結(jié)束而被釋放。
2)內(nèi)存分配與回收
內(nèi)存的靜態(tài)分配和動態(tài)分配的區(qū)別主要是兩個:
- 時間上:靜態(tài)分配發(fā)生在程序編譯和連接時,動態(tài)分配則發(fā)生在程序調(diào)入和執(zhí)行時。
- 空間上:堆都是動態(tài)分配的,沒有靜態(tài)分配的堆。棧有2種分配方式:靜態(tài)分配和動態(tài)分配。靜態(tài)分配是編譯器完成的,比如局部變量的分配。動態(tài)分配由函數(shù)malloc進行分配。不過棧的動態(tài)分配和堆不同,他的動態(tài)分配是由編譯器進行釋放,無需我們手工實現(xiàn)。
動態(tài)內(nèi)存分配與回收
所謂動態(tài)內(nèi)存分配,就是指在程序執(zhí)行的過程中動態(tài)地分配或者回收存儲空間的分配內(nèi)存的方法。動態(tài)內(nèi)存分配不象數(shù)組等靜態(tài)內(nèi)存分配方法那樣需要預(yù)先分配存儲空間,而是由系統(tǒng)根據(jù)程序的需要即時分配,且分配的大小就是程序要求的大小。
分配和回收操作函數(shù)介紹:
malloc:動態(tài)內(nèi)存分配,用于在堆上申請一塊連續(xù)的指定大小的內(nèi)存區(qū)域,但是并沒有初始化。
calloc:則將初始化這部分的內(nèi)存,設(shè)置為0. calloc = malloc + memset(初始化工作)。
alloca:是向棧申請內(nèi)存,因此無需釋放。
realloc:則對malloc申請的內(nèi)存進行大小的調(diào)整。
(注:這四個函數(shù)都是由free來釋放內(nèi)存。)
new :new 基于 malloc,卻又高于malloc,是它的一個提升版本。首先new不是庫函數(shù),它是一個關(guān)鍵字,通過new操作符申請的內(nèi)存都在自由存儲區(qū),且不需要指定內(nèi)存塊大小。內(nèi)存分配失敗時,會拋出bac_alloc異常,而不是返回一個NULL等等。new是通過delete來釋放內(nèi)存,它同樣也是一個關(guān)鍵字。
6.2.2 Java進程
從之前寫的系統(tǒng)啟動流程中我們了解了,zygote是java進程的鼻祖,它通過了Runtime啟動了虛擬機,并通過fork,把虛擬機作為環(huán)境帶給了每一個應(yīng)用進程。虛擬機的設(shè)計除了提供跨平臺能力之外,也提供了對象生命周期的管理,內(nèi)存管理,線程管理,安全和異常的管理等統(tǒng)一的處理方案。
1)內(nèi)存區(qū)域劃分
這部分之前文章有總結(jié):,如下是JVM內(nèi)存劃分模型,其實Dalvik和ART都一樣,就是Heap的space結(jié)構(gòu)會有區(qū)別:

程序計數(shù)器:是一塊較小的線程私有的內(nèi)存空間,用來記錄正在執(zhí)行的虛擬機字節(jié)碼指令,以此來記錄當(dāng)前線程的運行狀態(tài);它是一個指針,指向執(zhí)行引擎正在執(zhí)行的指令的地址。
虛擬機棧:棧是一塊連續(xù)的內(nèi)存區(qū)域,大小是由操作系統(tǒng)預(yù)定好的(2M左右),它是先進后出的隊列,進出一一對應(yīng),不產(chǎn)生碎片,運行效率穩(wěn)定高。局部變量的基本數(shù)據(jù)類型和引用存儲于棧中,因為它們屬于方法中的變量,生命周期隨方法而結(jié)束。
本地方法棧:針對Native方法的,功能與虛擬機棧一致。
靜態(tài)存儲區(qū)(方法區(qū)):內(nèi)存在程序編譯的時候就已經(jīng)分配好,這塊內(nèi)存在程序整個運行期間都存在。它主要存放靜態(tài)數(shù)據(jù)、全局static數(shù)據(jù)和包含常量池。
堆:堆是不連續(xù)的內(nèi)存區(qū)域(因為系統(tǒng)是用鏈表來存儲空閑內(nèi)存地址,自然不是連續(xù)的),堆大小受限于計算機系統(tǒng)中有效的虛擬內(nèi)存(32bit系統(tǒng)理論上是4G),所以堆的空間比較靈活,比較大。對于堆,頻繁的分配和回收內(nèi)存會造成大量內(nèi)存碎片,使程序效率降低。堆內(nèi)存用于存放引用的對象實體、成員變量全部存儲于堆中(包括基本數(shù)據(jù)類型,引用和引用的對象實體,因為它們屬于類,類對象終究是要被new出來使用的)。
Java內(nèi)存玩的就是虛擬機劃分的一畝三分地,而大小是由系統(tǒng)設(shè)置的,內(nèi)存的分配與回收都是虛擬機負責(zé)的。申請的內(nèi)存超過了一畝三分地就會oom,內(nèi)存不足會觸發(fā)gc,而gc又分串行g(shù)c與并行g(shù)c,art虛擬機優(yōu)化了gc環(huán)節(jié),大大縮短了全線程block的時長,但是如果明顯的內(nèi)存抖動還是會造成卡頓問題。
好了,虛擬機內(nèi)存管理暫時不分析了,之后有機會再單獨開系列來分析,內(nèi)存管理基礎(chǔ)暫時就寫這么多,歇了。
參考:
https://blog.csdn.net/jasonchen_gbd/article/details/79462014
http://www.wowotech.net/memory_management/233.html
https://www.ibm.com/developerworks/cn/linux/l-cn-pagerecycle/
https://www.cnblogs.com/fah936861121/p/6878699.html
https://blog.csdn.net/Luoshengyang/article/details/42492621
https://blog.csdn.net/Luoshengyang/article/details/42555483