前言
在執行iOS(OSX)程式時,左側的Debug Navigator
中可以看見當前使用的記憶體。我們也可以使用Instruments的Allocations
模板來追蹤物件的建立和釋放。不知道你是否也曾困惑於Debug Navigator
顯示的記憶體和Allocations
顯示的總記憶體對不上號的問題。本篇文章將帶你深入瞭解iOS的記憶體分配。
Allocations模版
在Instruments的Allocations
模板中,可以看到主要統計的是All Heap & Anonymous VM
的記憶體使用量。All Heap
好理解,就是App執行過程中在堆上分配的記憶體。我們可以通過搜尋關鍵字檢視你關注的類在堆上的記憶體分配情況。那麼Anonymous VM
是什麼呢?按照官方描述,它是和你的App程式關聯比較大的VM regions。原文如下。
interesting VM regions such as graphics- and Core Data-related. Hides mapped files, dylibs, and some large reserved VM regions.
複製程式碼
虛擬記憶體簡介
什麼是VM Regions呢?要知道這個首先要了解什麼是虛擬記憶體。當我們向系統申請記憶體時,系統並不會給你返回實體記憶體的地址,而是給你一個虛擬記憶體地址。每個程式都擁有相同大小的虛擬地址空間,對於32位的程式,可以擁有4GB的虛擬記憶體,64位程式則更多,可達18EB。只有我們開始使用申請到的虛擬記憶體時,系統才會將虛擬地址對映到實體地址上,從而讓程式使用真實的實體記憶體。下面是一個示意圖,我簡化了概念。
程式A和B都擁有1到4的虛擬記憶體。系統通過虛擬記憶體到實體記憶體的對映,讓A和B都可以使用到實體記憶體。上圖中實體記憶體是充足的,但是如果A佔用了大部分記憶體,B想要使用實體記憶體的時候實體記憶體卻不夠該怎麼辦呢?在OSX上系統會將不活躍的記憶體塊寫入硬碟,一般稱之為swapping out
。iOS上則會通知App,讓App清理記憶體,也就是我們熟知的Memory Warning。
記憶體分頁
系統會對虛擬記憶體和實體記憶體進行分頁,虛擬記憶體到實體記憶體的對映都是以頁為最小粒度的。在OSX和早期的iOS系統中,物理和虛擬記憶體都按照4KB的大小進行分頁。iOS近期的系統中,基於A7和A8處理器的系統,實體記憶體按照4KB分頁,虛擬記憶體按照16KB分頁。基於A9處理器的系統,物理和虛擬記憶體都是以16KB進行分頁。系統將記憶體頁分為三種狀態。
- 活躍記憶體頁(active pages)- 這種記憶體頁已經被對映到實體記憶體中,而且近期被訪問過,處於活躍狀態。
- 非活躍記憶體頁(inactive pages)- 這種記憶體頁已經被對映到實體記憶體中,但是近期沒有被訪問過。
- 可用的記憶體頁(free pages)- 沒有關聯到虛擬記憶體頁的實體記憶體頁集合。
當可用的記憶體頁降低到一定的閥值時,系統就會採取低記憶體應對措施,在OSX中,系統會將非活躍記憶體頁交換到硬碟上,而在iOS中,則會觸發Memory Warning,如果你的App沒有處理低記憶體警告並且還在後臺佔用太多記憶體,則有可能被殺掉。
VM Region
為了更好的管理記憶體頁,系統將一組連續的記憶體頁關聯到一個VMObject上,VMObject主要包含下面的屬性。
- Resident pages - 已經被對映到實體記憶體的虛擬記憶體頁列表
- Size - 所有記憶體頁所佔區域的大小
- Pager - 用來處理記憶體頁在硬碟和實體記憶體中交換問題
- Attributes - 這塊記憶體區域的屬性,比如讀寫的許可權控制
- Shadow - 用作(copy-on-write)寫時拷貝的優化
- Copy - 用作(copy-on-write)寫時拷貝的優化
我們在Instruments的
Anonymous VM
裡看到的每條記錄都是一個VMObject或者也可以稱之為VM Region
。
堆(heap)和 VM Region
那麼堆和VM Region是什麼關係呢?按照前面的說法,應該任何記憶體分配都逃不過虛擬記憶體這套流程,堆應該也是一個VM Region才對。我們應該怎樣才能知道堆和VM Region的關係呢?Instruments中有一個VM Track模版,可以幫助我們清楚的瞭解他們的關係。我建立了一個空的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,我們發現這個Region的Type是MALLOC_SMALL。點選箭頭看詳情,你將會看到這個Region中的記憶體頁列表。
可能你已經發現了,截圖中的記憶體頁Swapped列下都是被標記的,因為我測試的是Mac上的App,所以當記憶體頁不活躍時會被交換到硬碟上。這也就驗證了我們在上面提到的交換機制。如果我們將CustomObject
的尺寸變大,比如作如下變動。
@interface CustomObject() {
long a[20000];
}
@end
複製程式碼
記憶體上會有什麼變化呢?答案是CustomObject
會被移動到MALLOC_LARGE記憶體區。
所以總的來說,堆區會被劃分成很多不同的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執行過程中在堆上動態分配的記憶體頁都是Dirty的,載入動態庫或者檔案記憶體對映產生的記憶體頁則是非Dirty的。綜上,我們可以總結出,
Virtual Size >= Resident Size + Swapped Size >= Dirty Size + Swapped Size
,
malloc 和 calloc
我們除了使用NSObject的alloc分配記憶體外,還可以使用c的函式malloc進行記憶體分配。malloc的記憶體分配當然也是先分配虛擬記憶體,然後使用的時候再對映到實體記憶體,不過malloc有一個缺陷,必須配合memset將記憶體區中所有的值設定為0。這樣就導致了一個問題,malloc出一塊記憶體區域時,系統並沒有分配實體記憶體。然而,呼叫memset後,系統將會把malloc出的所有虛擬記憶體關聯到實體記憶體上,因為你訪問了所有記憶體區域。我們通過程式碼來驗證一下。在main方法中,建立一個1024*1024的記憶體塊,也就是1M。
void *memBlock = malloc(1024 * 1024);
複製程式碼
我們發現MALLOC_LARGE中有一塊虛擬記憶體大小為1M的VM Region。因為我們沒有使用這塊記憶體,所以其他Size都是0。現在我們加上memset再觀察。
void *memBlock = malloc(1024 * 1024);
memset(memBlock, 0, 1024 * 1024);
複製程式碼
現在Resident Size
,Dirty Size
也是1M了,說明這塊記憶體已經被對映到實體記憶體中去了。為了解決這個問題,蘋果官方推薦使用calloc代替malloc,calloc返回的記憶體區域會自動清零,而且只有使用時才會關聯到實體記憶體並清零。
malloc_zone_t 和 NSZone
相信大家對NSZone並不陌生,allocWithZone
或者copyWithZone
這2個方法大家應該也經常見到。那麼Zone究竟是什麼呢?Zone可以被理解為一組記憶體塊,在某個Zone裡分配的記憶體塊,會隨著這個Zone的銷燬而銷燬,所以Zone可以加速大量小記憶體塊的集體銷燬。不過NSZone實際上已經被蘋果拋棄。你可以建立自己的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];
}
}
複製程式碼
程式碼建立了1000個CustomObject
物件,並且嘗試使用新建的Zone。我們用heap工具看看結果。首先使用Activity Monitor找到程式的PID,在命令列中執行
heap PID
複製程式碼
執行的結果大致如下。
......
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)
就是我們建立的NSZone,不過它裡面只有一個節點,共32bytes,如果你不設定Zone的name,它會是0bytes。所以我們可以推匯出這32bytes是用來儲存Zone本身的資訊的。我們建立的1000個CustomObject
其實在Zone DefaultMallocZone_0x1004c9000
裡,也就是系統預設建立的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)系統中VM的相關原理,以及如何使用VM Track模板來分析VM Regions,本文只是關注了MALLOC相關的幾個VM Region,還有其他專用的一些VM Region,通過研究他們的記憶體分配,可以有針對性的對記憶體進行優化,這就是接下來要做的事情。