- 前言
- 經(jīng)典操作系統(tǒng)的虛擬內(nèi)存
- iOS的虛擬內(nèi)存
- 參考
- 寫在最后
前言
僅以此文解答自己大學(xué)以來多年對(duì)內(nèi)存管理的疑惑。
經(jīng)典操作系統(tǒng)的虛擬內(nèi)存
為什么要有虛擬內(nèi)存?
隨著計(jì)算機(jī)的發(fā)展,我們的計(jì)算機(jī)處理的任務(wù)也變得越來越繁多,但是對(duì)于某臺(tái)固定的計(jì)算機(jī),CPU 和 Memory 都是固定的,如果有些直接使用物理內(nèi)存地址的話會(huì)帶來很多問題, 首先編譯器不能以一種抽象的角度來描繪內(nèi)存,在執(zhí)行的過程中如果某個(gè)進(jìn)程占據(jù)的內(nèi)存過大,這個(gè)進(jìn)程可能就無法運(yùn)行,即便運(yùn)行了內(nèi)存相對(duì)來說是非常不安全的,一個(gè)不小心操作到了別的進(jìn)程的內(nèi)存,可能導(dǎo)致進(jìn)程的崩潰,如果寫入了內(nèi)核使用的內(nèi)存可能導(dǎo)致操作系統(tǒng)的崩潰。
現(xiàn)代操作系統(tǒng)的內(nèi)存管理是非常多計(jì)算機(jī)科學(xué)家智慧的結(jié)晶,這種管理方式就是 虛擬內(nèi)存 (Virtual Memory/VM). VM是一些列技術(shù)的總稱,包括硬件異常,物理之地翻譯,主存,磁盤文件,操作系統(tǒng)內(nèi)核軟件的內(nèi)存管理。
虛擬內(nèi)存提供了三大重要的特性
- 它將主存看做在存儲(chǔ)在磁盤上的地址空間的高速緩存,利用程序的局部性原理,只將活躍的內(nèi)存加載到主存中,提高了主存的利用率。
- 為每個(gè)進(jìn)程提高了一個(gè)抽象的統(tǒng)一的連續(xù)的私有的地址空間。簡(jiǎn)化了內(nèi)存管理方式。
- 對(duì)內(nèi)測(cè)進(jìn)行分段(segment)提供權(quán)限能力,保護(hù)每個(gè)進(jìn)程的地址空間不會(huì)被其他進(jìn)程影響。
尋址方式
在一些早期的操作系統(tǒng)和一些嵌入式操作系統(tǒng)中,內(nèi)存管理使用的地址是物理地址,
現(xiàn)代操作系統(tǒng)基本使用的是 虛擬地址(Virtual Addressing)的尋址方式,使用 虛擬地址時(shí) CPU將 VA 送到MMU中去翻譯為物理地址。
注: MMU (Memory Management Unit) 內(nèi)存管理單元一般是一個(gè)CPU上的專用芯片,是一個(gè)硬件。
結(jié)合操作系統(tǒng)共同完成地址翻譯工作。
地址空間
通常來說地址空間是線性的 假設(shè)我們有 {0, 1, 2, ..N-1 } 個(gè)內(nèi)存地址,我們可以用n位二進(jìn)制來表示內(nèi)存地址,那么我們就叫這個(gè)地址空間為n位地址空間, 現(xiàn)代操作系統(tǒng)通常是 32 或者 64(但是很多操作系統(tǒng)只用了48位尋址)的 。
2^10 = 1k
2^20 = 1M
2^30 = 1G
2^40 = 1T
2^50 = 1P
2^60 = 1E
這么看來大家能理解為什么32位的操作系統(tǒng)最大只支持4G內(nèi)存空間了。
分頁
現(xiàn)代操作系統(tǒng)將內(nèi)存劃分為頁,來簡(jiǎn)化內(nèi)存管理,一個(gè)頁其實(shí)就是一段連續(xù)的內(nèi)存地址的集合,通常有4k和16k(iOS 64位是16K)的,成為 Virtual Page 虛擬頁。與之對(duì)應(yīng)的物理內(nèi)存被稱為Physical Page 物理頁。
注意 虛擬頁的個(gè)數(shù)可能和物理頁個(gè)數(shù)不一樣 比如說一個(gè) 64位操作系統(tǒng)中使用48位地址空間的 虛擬頁大小為16K,那么其虛擬頁可數(shù)可達(dá)到(248/214 = 16M個(gè))假設(shè)物理內(nèi)存只有 4G 那么物理頁可能只有 (232/214 = 256k個(gè))
操作系統(tǒng)將虛擬頁和物理頁的映射關(guān)系稱為頁表(Page Table),每個(gè)映射叫 頁表?xiàng)l目(Page Table Entry/Item),操作系統(tǒng)為每個(gè)進(jìn)程提供一個(gè)頁表放在主存中,CPU在使用虛擬地址時(shí)交給MMU去翻譯地址,MMU去查詢?cè)谥鞔嬷械捻摫韥矸g。
缺頁處理
每個(gè) Page Table Entry 都包含了一些描述信息,比如前頁的狀態(tài){未分配, 緩存的,未緩存的}。
- 未分配的不用多說代表未使用的內(nèi)存
- 緩存的代表已經(jīng)加載進(jìn)物理內(nèi)存了
- 未緩存的代表還沒放在物理內(nèi)存。
當(dāng)CPU要讀取一個(gè)頁時(shí),檢查標(biāo)記發(fā)現(xiàn)當(dāng)前的頁是未緩存的,會(huì)觸發(fā)一個(gè)(Page Falut)缺頁中斷,這時(shí)內(nèi)核、、操作系統(tǒng)的缺頁異常處理程序,去選擇一個(gè)犧牲頁(有時(shí)候內(nèi)存夠用不用置換別的界面),然后檢查這個(gè)頁面是否有修改,有修改先寫會(huì)磁盤,然后將需要使用到的內(nèi)存加載到物理內(nèi)存中,然后更新PTE 隨后操作系統(tǒng)重新把虛擬地址發(fā)送到地址翻譯硬件去重新處理。
注: 有些操作系統(tǒng)無 虛擬虛擬內(nèi)存置換邏輯,如iOS,取而代之的是內(nèi)存壓縮,和收到內(nèi)存警告時(shí)殺死進(jìn)程的行為。
虛擬內(nèi)存帶來的好處
- 簡(jiǎn)化鏈接過程,允許每個(gè)進(jìn)程都提供統(tǒng)一的內(nèi)存地址的抽象,獨(dú)立與物理內(nèi)存。
- 簡(jiǎn)化加載,操作系統(tǒng)加載可執(zhí)行文件和共享文件時(shí),只是創(chuàng)建了 頁表,待訪問到缺頁時(shí),操作系統(tǒng)再去加載。
- 簡(jiǎn)化共享,不同進(jìn)程的PT中的PTE 可以執(zhí)行相同的物理地址,如動(dòng)態(tài)庫的代碼。
- 內(nèi)存保護(hù),PT中的PTE中描述了一個(gè)虛擬頁的權(quán)限信息,(R, W, X)、指令如果違反了這些權(quán)限信息,就會(huì)造成(Segment Fault)
地址翻譯
虛擬地址翻譯到物理地址是軟硬件結(jié)合實(shí)現(xiàn)的。我們通常幾個(gè)方面來描述。
如何索引
現(xiàn)代操作系統(tǒng)將地址分為兩部分,頁號(hào)和片了(是不是很類型網(wǎng)絡(luò)號(hào)和主機(jī)號(hào)),由于虛擬頁和物理頁的大小是相同的,頁偏移可以看做虛擬頁和物理頁的頁內(nèi)地址,且相同,頁號(hào)則做為PT的索引查找到對(duì)應(yīng)的PTE,然后查找對(duì)應(yīng)的物理頁地址。
提高效率
是不是像前面所說的簡(jiǎn)單的劃分位兩部分就足夠了呢?
舉個(gè)例子:
- 我們假設(shè)一臺(tái)電腦是 32 位的,分頁大小位 4k 也就說頁內(nèi)地址占據(jù)了 12 位,頁號(hào)地址位 20 位
- 我們假設(shè)一臺(tái)電腦是 64 位的,地址空間 48 位,分頁大小位 16k 也就說頁內(nèi)地址占據(jù)了 14 位,頁號(hào)地址位 34 位
我們粗略估算一個(gè)PTE為4KB 對(duì)于 32位的操作系統(tǒng)每個(gè)進(jìn)程的頁表需要 2^20 = 4M 個(gè)頁表項(xiàng)常駐內(nèi)存尚可接受
但是對(duì)于尋址為48位的操作系統(tǒng)來說,每個(gè)進(jìn)程的頁表為需要 2^32 = 4G個(gè)頁表項(xiàng),這是無法接受的。
計(jì)算機(jī)的世界所有的難題都可以同加一次的辦法來解決,所以現(xiàn)代操作系統(tǒng)通常都使用多級(jí)頁表,減少頁表項(xiàng)的個(gè)數(shù)。將虛擬地址分為多端,代表了一級(jí) 二級(jí) 多級(jí)頁表。通過多級(jí)頁表可以大大較少內(nèi)存占用。
減少內(nèi)存
眾所周知CPU要比Memory快10^3個(gè)數(shù)量級(jí),即便CPU中的L3Cache 也比Memory快很多,如果MMU美的地址翻譯都要去查找多級(jí)PT,這個(gè)開銷就會(huì)非常巨大,但是所幸 程序的局部性原理能夠解救我們,MMU芯片內(nèi)置一個(gè) 翻譯后備緩沖器(Transalation Lookaside Buffer TLB )的硬件來充當(dāng)緩存,加快地址翻譯的效率.
現(xiàn)代 OS 虛擬內(nèi)存系統(tǒng)
操作系統(tǒng)為每個(gè)進(jìn)程維護(hù)一個(gè)單獨(dú)的虛擬地址空間,分為兩部分。
- 內(nèi)核虛擬內(nèi)存,包含內(nèi)核中的代碼和數(shù)據(jù)結(jié)構(gòu),還有一些被映射到所有進(jìn)程共享的內(nèi)存頁面。還有一些頁表,內(nèi)核在進(jìn)程上下文中執(zhí)行代碼使用的棧。
- 進(jìn)程虛擬內(nèi)存。OS將內(nèi)存組織為一些區(qū)域(Sement)的集合,代碼端,數(shù)據(jù)端,共享庫端,線程棧都是不同的區(qū)域,分段的原因是便于管理內(nèi)存的權(quán)限,如果了解過Mach-O文件或者ELF文件的讀者可以看到相同的Segment里面的內(nèi)存權(quán)限是相同的,每個(gè)Segment再劃分不同的內(nèi)容為section。
在內(nèi)核中描述一個(gè)進(jìn)程的數(shù)據(jù)結(jié)構(gòu) 概略為如下
pgb指向第一級(jí)頁表的基址
進(jìn)程虛擬地址
vm_area_struct |----------------|
---- ---- |----> vm_end------| | |
mm ---> pgb | vm_start ---|---| | |
---- mmap --- vm_prot | |->|----------------|
---- vm_flags | | 共享庫 |
-- vm_next |----->|----------------|
| |
| |
|-> vm_end-----| |--> |----------------|
vm_start---|--| | 數(shù)據(jù)段 |
vm_prot |------>|----------------|
vm_flags |
每個(gè)區(qū)域的描述主要有一下幾個(gè)
vm_start 指向這個(gè)區(qū)域的起始處
vm_end 指向這個(gè)區(qū)域的結(jié)束出
vm_prot 內(nèi)存區(qū)域的讀寫權(quán)限
vm_flasg 一些標(biāo)志位 私有的還是共享的
vm_next 指向下一個(gè)vm_area_struct的描述
內(nèi)存映射 MMAP
類Unix操作系統(tǒng)可以荀彧映射一個(gè)普通磁盤上的文件的連續(xù)部分到一個(gè)固定的內(nèi)存取區(qū)域。操作系統(tǒng)會(huì)會(huì)自動(dòng)管理映射的內(nèi)容。
內(nèi)存映射允許不同的進(jìn)程映射不同的虛擬內(nèi)存到同一塊物理內(nèi)容上,他們可以是共享的也可以是私有的。
對(duì)于共享的,通常多個(gè)進(jìn)程映射到相同的共享對(duì)象上,
對(duì)與私有的,不同進(jìn)程初始映射的時(shí)候操作系統(tǒng)為了節(jié)省資源,并沒有產(chǎn)生真的副本,知道某個(gè)進(jìn)程修改了這個(gè)私有對(duì)象,操作系統(tǒng)運(yùn)用copy on write技術(shù)在此時(shí)才發(fā)生真正的文件拷貝。
mmap在類unix操作系統(tǒng)上作為一個(gè)系統(tǒng)調(diào)用存在,函數(shù)簽名如下
void *
mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);
addr 代表要從那塊虛擬地址開始映射,通??梢圆挥弥付▊鬟fNULL讓操作系統(tǒng)自己給我們選擇
len 映射多少長(zhǎng)度的內(nèi)容
prot 映射文件的訪問權(quán)限 讀寫可執(zhí)行權(quán)限等
PROT_EXEFC 可執(zhí)行權(quán)限
PROT_READ 可讀權(quán)限
PROT_WRITE 可寫權(quán)限
PROT_NONE 無法訪問權(quán)限
flags 訪問文件的標(biāo)記
MAP_SHARED 共享的
MAP_PRIVATE私有的
MAP_ANON 私有的
舉個(gè)例子將任意文件映射到stdout
#include <sys/mman.h>
int main(int argc, const char * argv[]) {
struct stat stat;
int fd;
if (argc != 2) {
printf("must pass file path");
return 1;
}
fd = open(argv[1], O_RDONLY, 0);
fstat(fd, &stat);
char *buffer = mmap(NULL, stat.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
printf("%s", buffer);
return 0;
}
MMAP在iOS中的用處
- mmap讓讀寫一個(gè)文件像操作一個(gè)內(nèi)存地址一樣簡(jiǎn)單方便,
- mmap效率極高,不用將一個(gè)內(nèi)容從磁盤讀入內(nèi)核態(tài)再拷貝至用戶態(tài)
- mmap映射的文件由操作系統(tǒng)接管,如果進(jìn)程Crash 操作系統(tǒng)會(huì)保證文件刷新回磁盤
動(dòng)態(tài)內(nèi)存分配
雖然可以使用上面的低級(jí)API去映射內(nèi)存,但是需要?jiǎng)討B(tài)申請(qǐng)內(nèi)存用來做變量處理的時(shí)候就需要?jiǎng)討B(tài)內(nèi)存分配器(Dunamic memory allocator)簡(jiǎn)單理解為 malloc calloc realloc free等函數(shù)來自的庫就稱為DMA.
動(dòng)態(tài)內(nèi)存分配器將一個(gè)內(nèi)存的區(qū)域(Heao)分為不同的大小的塊(block),這些塊要不然就是分配的,要不然就是空閑的。
如何設(shè)計(jì)分配器又是一個(gè)大難題。 幾乎所有的計(jì)算機(jī)語言都采用以下兩種。
- 顯式分配器(手動(dòng)管理內(nèi)容)
- 隱式分配器(GC)
隱式內(nèi)存分配器
通常比較知名的語言 Java javaScript Ruby 等都使用GC,最早的GC只是使用標(biāo)記清除算法來管理內(nèi)容,通過幾十年的迭代,早已更新出了數(shù)種算法共同參與的GC。這里就不再贅述了
顯式內(nèi)存分配器
C語言提供了一些列的方法來管理動(dòng)態(tài)內(nèi)存。如
- malloc 申請(qǐng)內(nèi)容并返回初始化的內(nèi)存首地址
- calloc 同malloc一致,并且會(huì)將申請(qǐng)到的內(nèi)存全置為0、
- realloc,重新分配原本已經(jīng)申請(qǐng)的內(nèi)存空間。
- free 釋放內(nèi)容空間
- sbrk 擴(kuò)展收縮堆
如何實(shí)現(xiàn)一個(gè)自己的顯式內(nèi)存分配器
首先我們要明確內(nèi)存分配器的需求
- 處理任意順序的申請(qǐng)內(nèi)存和釋放內(nèi)存
- 立即響應(yīng),不應(yīng)為了性能二重新排列或者緩存請(qǐng)求
- 所有內(nèi)容都在heap里存放
- 對(duì)齊塊,使之可以存放任意類型的數(shù)據(jù)
- 不修改已分配的內(nèi)存塊
鑒于對(duì)齊和處理任意順序內(nèi)存管理的需求,堆利用效率可能會(huì)降低,主要會(huì)產(chǎn)生內(nèi)存碎片(Fragmentation) 內(nèi)存碎片分為兩種。
- 內(nèi)部碎片,通常是指一個(gè)分配過的塊數(shù)據(jù)并不是全部塊的內(nèi)容,通常有元信息,對(duì)齊的字節(jié)等。
- 外部碎片是指不連續(xù)的可用的塊,通常外部碎片過多會(huì)產(chǎn)生,所有空白塊相加可以滿足申請(qǐng)的資源,但是他們不連續(xù)。需要整理碎片。
實(shí)現(xiàn)顯式內(nèi)存分配器的重點(diǎn)
- 空閑塊組織
- 如何分配新申請(qǐng)的塊
- 如果組織空閑快的剩余部分
- 如何合并剛釋放的塊
顯式內(nèi)存分配器的實(shí)現(xiàn)方案
隱式空閑鏈表
這種方式在malloc申請(qǐng)內(nèi)存的時(shí)候,實(shí)際上申請(qǐng)的是實(shí)際所需內(nèi)存加上部門元信息大小的塊,然后返回指針是有效數(shù)據(jù)的首地址,元信息直接存在數(shù)據(jù)塊中,所以稱為隱式空閑鏈表。
隱式鏈表需要處理如何分割空閑庫和合并空閑快
顯式空閑鏈表
由于隱式空閑鏈表的搜索效率角度,其實(shí)是不適用通用的內(nèi)存分配的??梢允褂媚撤N形式的數(shù)據(jù)結(jié)構(gòu)去管理這些內(nèi)存塊。
基本分為 幾種,
- 簡(jiǎn)單分離器存儲(chǔ)
- 分離適配法
- 伙伴系統(tǒng)法
關(guān)于詳細(xì)的設(shè)計(jì)需要讀者查看更多算法知識(shí)的文檔。
顯式內(nèi)存分配器的實(shí)現(xiàn)
顯式內(nèi)存分配器的需求已經(jīng)很清晰,下面有個(gè)簡(jiǎn)單的例子可以參考,這時(shí)候?qū)τ?C 類語言的內(nèi)存管理應(yīng)該不會(huì)太過恐懼了,
C++實(shí)現(xiàn)一個(gè)簡(jiǎn)易的內(nèi)存池分配器,
畢竟源碼面前了無秘密。
iOS的虛擬內(nèi)存
iOS 內(nèi)存的分頁大小
在arm64之后的芯片,操作系統(tǒng)通常使用16KB作為頁大小,我們寫的程序中的虛擬內(nèi)存地址右移動(dòng)14位則可得到頁編號(hào)。MMU通過TLB和固定在內(nèi)存進(jìn)程虛擬區(qū)域的頁表來翻譯來物理地址。
下面一份代碼可以獲取頁大小。
int main(int argc, char * argv[]) {
// 獲取虛擬內(nèi)存分頁數(shù)據(jù) 14為頁內(nèi)地址
printf("page-size%ld mask:%ld, shift%d \n", vm_kernel_page_size, vm_kernel_page_mask, vm_kernel_page_shift);
printf("%ld\n", sysconf(_SC_PAGE_SIZE));
printf("%d\n", getpagesize());
printf("%d\n", PAGE_SIZE); // 編譯時(shí)確定不建議使用
return 0;
}
在觀察Crash日志的時(shí)候 有時(shí)候注意崩潰的頁號(hào)可以幫助我們尋找崩潰的原因。
頁面的類型
當(dāng)操作系統(tǒng)分配一個(gè)頁面時(shí),內(nèi)存被稱為Clean的,以為這這個(gè)內(nèi)存頁面沒有使用,是可以被釋放或者重建的,但是一旦寫入,操作系統(tǒng)會(huì)將其標(biāo)記為Dirty,這意味著磁盤或者其他地方?jīng)]有此內(nèi)存頁面的備份,無法恢復(fù)它。
由于iPhone設(shè)備為了減少閃存的壽命,并沒有在閃存上使用交換分區(qū),因此無論使用多少,在內(nèi)存壓力高緊時(shí),操作系統(tǒng)不會(huì)將Dirty寫好磁盤,而是釋放Clean的頁面如,可執(zhí)行代碼(Mach-O)的映射和內(nèi)存映射文件,或者是kill掉進(jìn)程。
因此使用dirty的內(nèi)存越多,對(duì)我們的進(jìn)程的穩(wěn)定性越差。
iOS內(nèi)存的優(yōu)化
在其他常見的操作系統(tǒng)上,由于局部性原理,OS會(huì)將不常用的內(nèi)存頁面寫會(huì)磁盤,但是iOS沒有交換空間,取而代之的是內(nèi)存壓縮技術(shù),iOS 將不常用到的dirty頁面壓縮以減少頁面占用量,在再次訪問到的時(shí)候重新解壓縮。這些都在操作系統(tǒng)層面實(shí)現(xiàn),對(duì)進(jìn)程無感知,有趣的是如果當(dāng)前進(jìn)程收到了 memoryWarning, 進(jìn)程這時(shí)候準(zhǔn)備釋放大量的誤用內(nèi)存,如果訪問到過多的壓縮內(nèi)存,再解壓縮內(nèi)存的時(shí)候反而會(huì)導(dǎo)致內(nèi)存壓力更大,然后被OS kill掉。
iOS 進(jìn)程中的堆和棧
需要注意的是通常操作系統(tǒng)書籍中描述的進(jìn)程虛擬內(nèi)存模型都是這樣的
[圖片上傳失敗...(image-a1d525-1538764389522)]
這實(shí)際是個(gè)用于解析給讀者的簡(jiǎn)化模型,對(duì)于多線程程序來說,每個(gè)線程都有自己的線程棧,在iOS上通常主線程線程棧大小為1MB,子線程棧大小為 512KB,如果你有一臺(tái)越獄機(jī) 可以試驗(yàn) ulimt -a
命令觀察棧大小的默認(rèn)參數(shù)。
iOS平臺(tái)上的常見編程語言的內(nèi)存管理方式
iOS 上常用的Swift 和 Objective-C ,C , C++ 都使用顯式的內(nèi)存管理?策略,比如 malloc 和 free, new 和delete alloc 和 dealloc,在Objective-C和Swift通常使用一種叫做引用計(jì)數(shù)的簡(jiǎn)化模型來管理堆內(nèi)存?,F(xiàn)代Clang已經(jīng)支持ARC的技術(shù)幫助程序員解脫內(nèi)存管理的困擾,但是本質(zhì)上還是顯式內(nèi)存管理。
建議讀者可以讀一下ARC的參考文檔,
順便提一下Xcode10 版本中的Clang已經(jīng)支持在C結(jié)構(gòu)體中對(duì)于Objective-C對(duì)象的ARC管理,請(qǐng)參看 whats_new_in_llvm
內(nèi)存分類
要想合理的使用內(nèi)存必須要掌握不同類型內(nèi)存的區(qū)別,才能更合理的使用內(nèi)存并且在內(nèi)存資源匱乏的低端機(jī)器上寫出“高內(nèi)存性能”的應(yīng)用。
首先在 Apple 的官方文檔中內(nèi)存主要分為以下幾類。
- Free Memory 當(dāng)前空閑的memory
- Used Mamory 當(dāng)前正在使用的內(nèi)存
我們最關(guān)心的當(dāng)然是 Used Memory,它又分為以下幾類。
- Wired Memory。 一般是內(nèi)核占用的常駐內(nèi)存,比如可執(zhí)行文件的鏡像 Image,內(nèi)核所有的數(shù)據(jù)等,無法釋放,在OS運(yùn)行期間必須常駐內(nèi)存。
- Active Memory 活躍的內(nèi)存,當(dāng)前正在使用的內(nèi)存.
- Inactive Memory。不活躍的內(nèi)存,最近用過,但是現(xiàn)在不怎么用了,按照局部性原則可以被置換出物理內(nèi)存的內(nèi)存。
- Purgeable Memory??舍尫诺膬?nèi)存,通常在Foundation中是
NSDiscardableContent
的子類,或者是NSCache
等。
等等。上面說的好像跟沒說一樣/(ㄒoㄒ)/~。我們換種方式從物理內(nèi)存和虛擬內(nèi)存的層面來解釋。
首先我們的虛擬內(nèi)存使用的是Page來描述的。 一個(gè)Page 有兩種狀態(tài) Dirty 和 Clean。在iOS中Clean是可以被回收的。
Virtual Memory分類
- Clean Memory 主要包括 system framework、binary executable 、memory mapped files
- Dirty Memory 括Heap allocation、caches、decompressed images等。
(每個(gè)進(jìn)程擁有一份獨(dú)立的 Virtual memory pace)Virtual Memory = clean Memory
PhySical Memory
物理內(nèi)存是指真正加載在主存中的內(nèi)存,所以實(shí)際了解上真正的物理內(nèi)存占用才對(duì)我們內(nèi)存管理幫助更大。
- DirtyMemory
- Clean Memory but loaded。
- Page Table
- ComPressed memory
- IOKit Used
- Purgeable
內(nèi)存測(cè)量工具
了解到前面說的內(nèi)存分類之后我們應(yīng)該怎么測(cè)量我們的內(nèi)存分布呢。主要有幾種工具,命令行工具,Xcode工具,代碼工具等。
命令行工具
如果你開發(fā)的是 Mac 程序,Mac OS 自帶的有一下幾種。
- top 程序
- heap 程序
- leaks 程序
- vmmap 程序
這些工具讀者查看 Man Page 即可。
需要注意的是。以上工具分析的大多是虛擬內(nèi)存,也就是說對(duì)于 桌面級(jí)程序更適合,但是對(duì)于iOS中沒有交換空間,且擁有Jetsam監(jiān)控程序的設(shè)備,可能還需要更精準(zhǔn)的測(cè)量工具。
順便提一句。一個(gè)堆區(qū)上malloc的程序如果并沒有使用,雖然它是Clean的,但是也會(huì)被程序統(tǒng)計(jì)到,理論上 malloc 可以申請(qǐng)到的虛擬內(nèi)存大小非常接近 virtual Memory Space 的大?。ㄟ@么說的原因是 前文也提到了 malloc 實(shí)際上是動(dòng)態(tài)分配器程序提供的一些列函數(shù),為了性能,大多數(shù)動(dòng)態(tài)分配器都講堆分為好幾塊用來做不同大小虛擬內(nèi)存的管理,因此malloc可以申請(qǐng)到的虛擬內(nèi)存大小實(shí)際決定于動(dòng)分配器代碼的實(shí)現(xiàn)。有興趣的讀者可以讀一下。)
Xcode 提供的工具
- Xcode Debug Area
- Instruments
- DebugMemoryGraph
Tips 配置了 MallocStackLogging 的話甚至可以追蹤每個(gè) 虛擬內(nèi)存中的對(duì)象申請(qǐng)堆棧,便于我們更好的發(fā)現(xiàn)問題。
注意點(diǎn):所有Xcode提供的工具必須使用真機(jī)測(cè)試才能最難接近用戶的使用環(huán)境
代碼工具
我們通過開發(fā)工具可以用來測(cè)量我們的內(nèi)存,但是到了線上這些都用不了,能精準(zhǔn)的測(cè)量APP用到的物理內(nèi)存才比較重要。
大部分的代碼測(cè)量?jī)?nèi)存是通過拿到Mach內(nèi)核提供的 task_info 來測(cè)量的,但是這個(gè)信息更多的是虛擬內(nèi)存層面的信息不能正確的衡量物理內(nèi)存。
#include <malloc/malloc.h>
#include <mach/mach_host.h>
#include <mach/task.h>
int main(int argc, char * argv[]) {
@autoreleasepool {
// method 1
struct mstats currentStat = mstats();
printf("Freed Bytes:%ld, Used Bytes:%ld Total Bytes:%ld", currentStat.bytes_free, currentStat.bytes_used, currentStat.bytes_total);
// method 2
vm_statistics_data_t vmStats;
mach_msg_type_number_t infoCount = HOST_VM_INFO_COUNT;
kern_return_t kernReturn = host_statistics(mach_host_self(), HOST_VM_INFO, (host_info_t)&vmStats, &infoCount);
printf("free: %lu\nactive: %lu\ninactive: %lu\nwire: %lu\nzero fill: %lu\nreactivations: %lu\npageins: %lu\npageouts: %lu\nfaults: %u\ncow_faults: %u\nlookups: %u\nhits: %u",
vmStats.free_count * vm_page_size,
vmStats.active_count * vm_page_size,
vmStats.inactive_count * vm_page_size,
vmStats.wire_count * vm_page_size,
vmStats.zero_fill_count * vm_page_size,
vmStats.reactivations * vm_page_size,
vmStats.pageins * vm_page_size,
vmStats.pageouts * vm_page_size,
vmStats.faults,
vmStats.cow_faults,
vmStats.lookups,
vmStats.hits
);
// method3
task_basic_info_data_t taskInfo;
infoCount = TASK_BASIC_INFO_COUNT;
kernReturn = task_info(mach_task_self(),
TASK_BASIC_INFO,
(task_info_t)&taskInfo,
&infoCount);
if (kernReturn == KERN_SUCCESS) {
printf("resdientSize is :%ld", taskInfo.resident_size);
}
return 0;
}
}
其中尤其是和Xcode Debug Area的差距較大有時(shí)候可能會(huì)偏差 50M-100M ,于是有大佬拔出了Xcode 的 DebugServer 和 WebKit 中的的物理內(nèi)存計(jì)算方式(2018WWDC 蘋果也說了 footPrint才是真正的物理內(nèi)存使用ios_memory_deep_dive)
代碼如下
std::optional<size_t> memoryFootprint()
{
task_vm_info_data_t vmInfo;
mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
kern_return_t result = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t) &vmInfo, &count);
if (result != KERN_SUCCESS)
return std::nullopt;
return static_cast<size_t>(vmInfo.phys_footprint);
}
線上檢查工具
線上檢查內(nèi)存通常會(huì)檢查內(nèi)存泄漏,一般有開源的工具
高性能使用內(nèi)存
了解完那么多原理和分析的工具,那么在日常使用中有沒有什么指導(dǎo)原則可以幫助我們來寫出更快,內(nèi)存占用更低的代碼呢?
- 首先熟讀 ARCMenual ,大部分iOS開發(fā)者其實(shí)是完全不清楚ARC是怎么實(shí)現(xiàn)的,還有相對(duì)于的原則,尤其是Autorelease 修飾的指針,還有在多線程情況下的原則。
- 用weak修飾替換unsafe_unretain
- 使用weak strong dance 來解決block中的循環(huán)引用問題。需要注意的是大部分人都以為使用了weak指針就可以了。其實(shí)不然,在block內(nèi)必須使用 strong 重新綁定變量,避免在多線程情況下weak變量為空導(dǎo)致Crash,使用strong指針前判斷是否為空
例:
- (void)test {
weak __typeof(self) weakSelf = self;
[xxobjc onCompleate:^(){
strong __typeof(self) self = weakSelf;
if (!self) { return; }
[xx moreCompleate:&(){
strong __typeof(self) self = weakSelf;
if (!self) { return; }
// do something
}];
}];
}
- 小心方法中的self,在Objective-C的方法中 隱含的 self 是 __unsafed_unretain的。
- 使用Autoreleasepool來降低循環(huán)中的內(nèi)存峰值,避免OOM。
- 要處理 Memory Warning
- C/C++ new 出來的要delete malloc 的要 free。
- UITableView/UICollectionView 的重用(不單單是cell重用 cell 使用的子view也要重用。
- [UIImage imageNamed:] 適合于UI界面中的貼圖的讀取,較大的資源文件應(yīng)該盡量避免使用。
- WKWebView是跨進(jìn)程通信的,不會(huì)占用我們的APP使用的物理內(nèi)存量。
- try_catch_finally 一定要清理資源
- 盡量少引用
performaSelector:
會(huì)對(duì)ARC的內(nèi)存管理產(chǎn)生錯(cuò)誤,導(dǎo)致內(nèi)存泄漏。 - lazy load 那些大的內(nèi)存對(duì)象, 尤其是需要保證線程安全,可以參考 java中的懶漢式Double Check 寫法。
- 需要在收到內(nèi)存警告的時(shí)候釋放的Cache 用 NSCache 代替 NSDictionary, 使用 NSPurgableData代替NSData.
前文中我們說到iOS的沒有交換分區(qū)的概念,取而代之的是壓縮內(nèi)存的辦法,倘若在使用NSDictionary的時(shí)候收到內(nèi)存警告,然后去釋放這個(gè)NSDictionary,如果占據(jù)的內(nèi)存過大,很可能在解壓的過程中就被JetSem Kill 掉,如果你的內(nèi)存只是誤用的緩存或者是可重建的數(shù)據(jù),就把NSCache當(dāng)初NSDictionary用吧。同理 NSPurableData也是。
- 不要使用像素過大的圖片文件,即便一個(gè)圖片在磁盤中很小,但是因?yàn)閳D片像素寬高很大也會(huì)占據(jù)更多的內(nèi)存,這里有個(gè)公式可以計(jì)算
widthPx * HeightPx * 4Bytes per pixel(alpha red green blue)
.即便在iOS12中已經(jīng)可以優(yōu)化單色圖的內(nèi)存占用,可畢竟是iOS12,現(xiàn)在好多公司還在支持iOS8 ~~ - 使用 NSData和UIImage 的 mmap加載選型來加載那些可以被重建的數(shù)據(jù)。
- 在子線程手動(dòng)申請(qǐng)(maloc)大內(nèi)存的的時(shí)候ping一下主線程,因?yàn)樽泳€程無法收到內(nèi)存警告的傳遞
- (void)test {
// current on sub Thread
// if main thread is memory warning it will blocked
dispatch_sync(dispatch_get_main_queue(), ^{
[some description]
});
malloc(huge memory);
}
參考
- 深入理解計(jì)算機(jī)系統(tǒng)
- 高性能iOS應(yīng)用開發(fā)
- iOS和macOS性能優(yōu)化:Cocoa、Cocoa Touch、Objective-C和Swift
- WWDC iOS Memory Deep Dive
- C++實(shí)現(xiàn)一個(gè)簡(jiǎn)易的內(nèi)存池分配器
- ARC的參考文檔
- whats_new_in_llvm
- 先弄清楚這里的學(xué)問,再來談 iOS 內(nèi)存管理與優(yōu)化(一)
- 先弄清楚這里的學(xué)問,再來談 iOS 內(nèi)存管理與優(yōu)化(二)
- 讓人懵逼的 iOS 系統(tǒng)內(nèi)存分配問題
- 探索iOS內(nèi)存分配
- iOS內(nèi)存深入探索之VM Tracker
- iOS內(nèi)存深入探索之Leaks
- iOS內(nèi)存深入探索之內(nèi)存用量
- iOS筆記-記錄一次內(nèi)存泄漏發(fā)現(xiàn)過程
- iOS 內(nèi)存管理及優(yōu)化
- Memory Usage Performance Guidelines
- Performance Overview
- Debugging with Xcode
- Threading Programming Guide
- LLDB Quick Start Guide
- LLDB Debugging Guide
- Instruments Help Topics
- Advanced Memory Management Programming Guide
- Exception Programming Topics
- 小試Xcode逆向:app內(nèi)存監(jiān)控原理初探
- osfmk/kern/task.c
- MacOSX/MachTask.mm
- No pressure, Mon!
寫在最后
現(xiàn)在做 iOS 開發(fā)太難了