前言
在運行iOS(OSX)程序時恶迈,左側的Debug Navigator
中可以看見當前使用的內存电湘。我們也可以使用Instruments的Allocations
模板來追蹤對象的創(chuàng)建和釋放。不知道你是否也曾困惑于Debug Navigator
顯示的內存和Allocations
顯示的總內存對不上號的問題。本篇文章將帶你深入了解iOS的內存分配廓八。
Allocations模版
在Instruments的Allocations
模板中,可以看到主要統(tǒng)計的是All Heap & Anonymous VM
的內存使用量赵抢。All Heap
好理解剧蹂,就是App運行過程中在堆上分配的內存。我們可以通過搜索關鍵字查看你關注的類在堆上的內存分配情況烦却。那么Anonymous VM
是什么呢宠叼?按照官方描述,它是和你的App進程關聯(lián)比較大的VM regions其爵。原文如下冒冬。
interesting VM regions such as graphics- and Core Data-related. Hides mapped files, dylibs, and some large reserved VM regions.
虛擬內存簡介
什么是VM Regions呢?要知道這個首先要了解什么是虛擬內存摩渺。當我們向系統(tǒng)申請內存時简烤,系統(tǒng)并不會給你返回物理內存的地址,而是給你一個虛擬內存地址摇幻。每個進程都擁有相同大小的虛擬地址空間横侦,對于32位的進程,可以擁有4GB的虛擬內存绰姻,64位進程則更多枉侧,可達18EB。只有我們開始使用申請到的虛擬內存時龙宏,系統(tǒng)才會將虛擬地址映射到物理地址上棵逊,從而讓程序使用真實的物理內存。下面是一個示意圖银酗,我簡化了概念辆影。
進程A和B都擁有1到4的虛擬內存。系統(tǒng)通過虛擬內存到物理內存的映射黍特,讓A和B都可以使用到物理內存蛙讥。上圖中物理內存是充足的,但是如果A占用了大部分內存灭衷,B想要使用物理內存的時候物理內存卻不夠該怎么辦呢次慢?在OSX上系統(tǒng)會將不活躍的內存塊寫入硬盤,一般稱之為swapping out
。iOS上則會通知App迫像,讓App清理內存劈愚,也就是我們熟知的Memory Warning。
內存分頁
系統(tǒng)會對虛擬內存和物理內存進行分頁闻妓,虛擬內存到物理內存的映射都是以頁為最小粒度的菌羽。在OSX和早期的iOS系統(tǒng)中,物理和虛擬內存都按照4KB的大小進行分頁由缆。iOS近期的系統(tǒng)中注祖,基于A7和A8處理器的系統(tǒng),物理內存按照4KB分頁均唉,虛擬內存按照16KB分頁是晨。基于A9處理器的系統(tǒng)舔箭,物理和虛擬內存都是以16KB進行分頁罩缴。系統(tǒng)將內存頁分為三種狀態(tài)。
- 活躍內存頁(active pages)- 這種內存頁已經被映射到物理內存中,而且近期被訪問過,處于活躍狀態(tài)匣摘。
- 非活躍內存頁(inactive pages)- 這種內存頁已經被映射到物理內存中,但是近期沒有被訪問過炉抒。
- 可用的內存頁(free pages)- 沒有關聯(lián)到虛擬內存頁的物理內存頁集合。
當可用的內存頁降低到一定的閥值時稚叹,系統(tǒng)就會采取低內存應對措施焰薄,在OSX中,系統(tǒng)會將非活躍內存頁交換到硬盤上扒袖,而在iOS中塞茅,則會觸發(fā)Memory Warning,如果你的App沒有處理低內存警告并且還在后臺占用太多內存季率,則有可能被殺掉野瘦。
VM Region
為了更好的管理內存頁,系統(tǒng)將一組連續(xù)的內存頁關聯(lián)到一個VMObject上飒泻,VMObject主要包含下面的屬性鞭光。
- Resident pages - 已經被映射到物理內存的虛擬內存頁列表
- Size - 所有內存頁所占區(qū)域的大小
- Pager - 用來處理內存頁在硬盤和物理內存中交換問題
- Attributes - 這塊內存區(qū)域的屬性,比如讀寫的權限控制
- Shadow - 用作(copy-on-write)寫時拷貝的優(yōu)化
- Copy - 用作(copy-on-write)寫時拷貝的優(yōu)化
我們在Instruments的Anonymous VM
里看到的每條記錄都是一個VMObject或者也可以稱之為VM Region
泞遗。
堆(heap)和 VM Region
那么堆和VM Region是什么關系呢惰许?按照前面的說法,應該任何內存分配都逃不過虛擬內存這套流程史辙,堆應該也是一個VM Region才對汹买。我們應該怎樣才能知道堆和VM Region的關系呢佩伤?Instruments中有一個VM Track模版,可以幫助我們清楚的了解他們的關系晦毙。我創(chuàng)建了一個空的Command Line Tool App生巡。
使用下面的代碼。
int main(int argc, const char * argv[]) {
NSMutableSet *objs = [NSMutableSet new];
@autoreleasepool {
for (int i = 0; i < 1000; ++i) {
CustomObject *obj = [CustomObject new];
[objs addObject:obj];
}
sleep(100000);
}
return 0;
}
CustomObject
是一個簡單的OC類见妒,只包含一個long類型的數組屬性障斋。
@interface CustomObject() {
long a[200];
}
@end
運行Profile,選擇Allocation模版徐鹤,進入后再添加VM Track模版,這里不知道為什么Allocation模版自帶的VM Track不工作邀层,只能自己手動加一個了返敬。
我們在All Heap & Anonymous VM
下可以看到,CustomObject
有1000個實例寥院,點擊CustomObject
右邊的箭頭劲赠,查看對象地址。
第一個地址是0x7faab2800000
秸谢。我們切換到最底下的VM Track凛澎,將模式調整為Regions Map。
然后找到Address Range為0x7faab2800000
開頭的Region估蹄,我們發(fā)現(xiàn)這個Region的Type是MALLOC_SMALL塑煎。點擊箭頭看詳情,你將會看到這個Region中的內存頁列表臭蚁。
可能你已經發(fā)現(xiàn)了最铁,截圖中的內存頁Swapped列下都是被標記的,因為我測試的是Mac上的App垮兑,所以當內存頁不活躍時會被交換到硬盤上冷尉。這也就驗證了我們在上面提到的交換機制。如果我們將CustomObject
的尺寸變大系枪,比如作如下變動雀哨。
@interface CustomObject() {
long a[20000];
}
@end
內存上會有什么變化呢?答案是CustomObject
會被移動到MALLOC_LARGE內存區(qū)私爷。
所以總的來說雾棺,堆區(qū)會被劃分成很多不同的VM Region,不同類型的內存分配根據需求進入不同的VM Region衬浑。除了MALLOC_LARGE和MALLOC_SMALL外垢村,還有MALLOC_TINY, MALLOC metadata等等嚎卫。具體什么樣的內存分配進什么樣的VM Region嘉栓,我自己也還在探索中宏榕。
VM Region Size
我們在VM Track中可以看到,一個VM Region有4種size侵佃。
- Dirty Size
- Swapped Size
- Resident Size
- Virtual Size
Virtual Size
顧名思義麻昼,就是虛擬內存大小,將一個VM Region的結束地址減去起始地址就是這個值馋辈。Resident Size
指的是實際使用物理內存的大小抚芦。Swapped Size
則是交換到硬盤上的大小,僅OSX可用迈螟。Dirty Size
根據官方的解釋我的理解是如果一個內存頁想要被復用叉抡,必須將內容寫到硬盤上的話,這個內存頁就是Dirty的答毫。下面是官方對Dirty Size
的解釋褥民。secondary storage
可以理解為硬盤。
The amount of memory currently being used that must be written to secondary storage before being reused.
所以一般來說app運行過程中在堆上動態(tài)分配的內存頁都是Dirty的洗搂,加載動態(tài)庫或者文件內存映射產生的內存頁則是非Dirty的消返。綜上,我們可以總結出耘拇,
Virtual Size >= Resident Size + Swapped Size >= Dirty Size + Swapped Size
撵颊,
malloc 和 calloc
我們除了使用NSObject的alloc分配內存外,還可以使用c的函數malloc進行內存分配惫叛。malloc的內存分配當然也是先分配虛擬內存倡勇,然后使用的時候再映射到物理內存,不過malloc有一個缺陷嘉涌,必須配合memset將內存區(qū)中所有的值設置為0译隘。這樣就導致了一個問題,malloc出一塊內存區(qū)域時洛心,系統(tǒng)并沒有分配物理內存固耘。然而,調用memset后词身,系統(tǒng)將會把malloc出的所有虛擬內存關聯(lián)到物理內存上厅目,因為你訪問了所有內存區(qū)域。我們通過代碼來驗證一下法严。在main方法中损敷,創(chuàng)建一個1024*1024的內存塊,也就是1M深啤。
void *memBlock = malloc(1024 * 1024);
我們發(fā)現(xiàn)MALLOC_LARGE中有一塊虛擬內存大小為1M的VM Region拗馒。因為我們沒有使用這塊內存,所以其他Size都是0∷萁郑現(xiàn)在我們加上memset再觀察诱桂。
void *memBlock = malloc(1024 * 1024);
memset(memBlock, 0, 1024 * 1024);
現(xiàn)在Resident Size
洋丐,Dirty Size
也是1M了,說明這塊內存已經被映射到物理內存中去了挥等。為了解決這個問題友绝,蘋果官方推薦使用calloc代替malloc,calloc返回的內存區(qū)域會自動清零肝劲,而且只有使用時才會關聯(lián)到物理內存并清零迁客。
malloc_zone_t 和 NSZone
相信大家對NSZone并不陌生,allocWithZone
或者copyWithZone
這2個方法大家應該也經常見到辞槐。那么Zone究竟是什么呢掷漱?Zone可以被理解為一組內存塊,在某個Zone里分配的內存塊榄檬,會隨著這個Zone的銷毀而銷毀卜范,所以Zone可以加速大量小內存塊的集體銷毀。不過NSZone實際上已經被蘋果拋棄丙号。你可以創(chuàng)建自己的NSZone,然后使用allocWithZone
將你的OC對象在這個NSZone上分配缰冤,但是你的對象還是會被分配在默認的NSZone里犬缨。我們可以用heap工具查看進程的Zone分布情況。首先使用下面的代碼讓CustomObject
使用新的NSZone棉浸。
void allocCustomObjectsWithCustomNSZone() {
static NSMutableSet *objs = nil;
if (objs == nil) { objs = [NSMutableSet new]; }
NSZone *customZone = NSCreateZone(1024, 1024, YES);
NSSetZoneName(customZone, @"Custom Object Zone");
for (int i = 0; i < 1000; ++i) {
CustomObject *obj = [CustomObject allocWithZone:customZone];
[objs addObject:obj];
}
}
代碼創(chuàng)建了1000個CustomObject
對象怀薛,并且嘗試使用新建的Zone。我們用heap工具看看結果迷郑。首先使用Activity Monitor找到進程的PID枝恋,在命令行中執(zhí)行
heap PID
執(zhí)行的結果大致如下。
......
Process 25073: 3 zones
Zone DefaultMallocZone_0x1004c9000: Overall size: 196992KB; 13993 nodes malloced for 160779KB (81% of capacity); largest unused: [0x102800000-171072KB]
Zone Custom Object Zone_0x1004fe000: Overall size: 1024KB; 1 nodes malloced for 1KB (0% of capacity); largest unused: [0x102200000-1024KB]
Zone GFXMallocZone_0x1004d8000: Overall size: 0KB
All zones: 13994 nodes malloced - 160779KB
Zone DefaultMallocZone_0x1004c9000: 13993 nodes - Sizes: 160KB[1000] 64.5KB[1] 16.5KB[1] 13.5KB[1] 4.5KB[3] 2KB[3] 1.5KB[12] 1KB[1] 704[1] 576[13] 528[4] 512[2] 480[1] 464[1] 448[2] 432[1] 400[1] 384[2] 368[1] 352[1] 336[2] 320[1] 272[8] 256[1] 240[4] 208[10] 192[5] 176[3] 160[5] 144[28] 128[48] 112[43] 96[83] 80[519] 64[3044] 48[5415] 32[3640] 16[82]
Zone Custom Object Zone_0x1004fe000: 1 nodes - Sizes: 32[1]
Zone GFXMallocZone_0x1004d8000: 0 nodes
All zones: 13994 nodes malloced - Sizes: 160KB[1000] 64.5KB[1] 16.5KB[1] 13.5KB[1] 4.5KB[3] 2KB[3] 1.5KB[12] 1KB[1] 704[1] 576[13] 528[4] 512[2] 480[1] 464[1] 448[2] 432[1] 400[1] 384[2] 368[1] 352[1] 336[2] 320[1] 272[8] 256[1] 240[4] 208[10] 192[5] 176[3] 160[5] 144[28] 128[48] 112[43] 96[83] 80[519] 64[3044] 48[5415] 32[3641] 16[82]
Found 523 ObjC classes
Found 56 CFTypes
-----------------------------------------------------------------------
Zone DefaultMallocZone_0x1004c9000: 13993 nodes (164637440 bytes)
COUNT BYTES AVG CLASS_NAME TYPE BINARY
===== ===== === ========== ==== ======
12771 779136 61.0 non-object
1000 163840000 163840.0 CustomObject ObjC VMResearch
49 2864 58.4 CFString ObjC CoreFoundation
21 1344 64.0 pthread_mutex_t C libpthread.dylib
20 1280 64.0 CFDictionary ObjC CoreFoundation
18 2368 131.6 CFDictionary (Value Storage) C CoreFoundation
16 2304 144.0 CFDictionary (Key Storage) C CoreFoundation
8 512 64.0 CFBasicHash CFType CoreFoundation
7 560 80.0 CFArray ObjC CoreFoundation
6 768 128.0 CFPrefsPlistSource ObjC CoreFoundation
6 480 80.0 OS_os_log ObjC libsystem_trace.dylib
5 160 32.0 NSMergePolicy ObjC CoreData
4 384 96.0 NSLock ObjC Foundation
......
-----------------------------------------------------------------------
Zone Custom Object Zone_0x1004fe000: 1 nodes (32 bytes)
COUNT BYTES AVG CLASS_NAME TYPE BINARY
===== ===== === ========== ==== ======
1 32 32.0 non-object
-----------------------------------------------------------------------
Zone GFXMallocZone_0x1004d8000: 0 nodes (0 bytes)
一共有3個zone嗡害,Zone Custom Object Zone_0x1004fe000: 1 nodes (32 bytes)
就是我們創(chuàng)建的NSZone焚碌,不過它里面只有一個節(jié)點,共32bytes霸妹,如果你不設置Zone的name十电,它會是0bytes。所以我們可以推導出這32bytes是用來存儲Zone本身的信息的叹螟。我們創(chuàng)建的1000個CustomObject
其實在Zone DefaultMallocZone_0x1004c9000
里鹃骂,也就是系統(tǒng)默認創(chuàng)建的NSZone。如果你真的想用Zone內存機制罢绽,可以使用malloc_zone_t畏线。通過下面的代碼可以在自定義的zone上malloc內存塊。
void allocCustomObjectsWithCustomMallocZone() {
malloc_zone_t *customZone = malloc_create_zone(1024, 0);
malloc_set_zone_name(customZone, "custom malloc zone");
for (int i = 0; i < 1000; ++i) {
malloc_zone_malloc(customZone, 300 * 4096);
}
}
再次使用heap工具查看良价。我只截取了custom malloc zone的內容寝殴。有1001個node蒿叠,也就是1000個malloc_zone_malloc出來的內存塊加上zone本身的信息所占的內存塊。
-----------------------------------------------------------------------
Zone custom malloc zone_0x1004fe000: 1001 nodes (1228800032 bytes)
COUNT BYTES AVG CLASS_NAME TYPE BINARY
===== ===== === ========== ==== ======
1001 1228800032 1227572.4 non-object
我們可以使用malloc_destroy_zone(customZone)
一次性釋放上面分配的所有內存杯矩。
總結
本文主要介紹了iOS (OSX)系統(tǒng)中VM的相關原理栈虚,以及如何使用VM Track模板來分析VM Regions,本文只是關注了MALLOC相關的幾個VM Region史隆,還有其他專用的一些VM Region魂务,通過研究他們的內存分配,可以有針對性的對內存進行優(yōu)化泌射,這就是接下來要做的事情粘姜。