探索iOS內存分配

前言

在運行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)才會將虛擬地址映射到物理地址上棵逊,從而讓程序使用真實的物理內存。下面是一個示意圖银酗,我簡化了概念辆影。


111516112448_.pic.jpg

進程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)。

  1. 活躍內存頁(active pages)- 這種內存頁已經被映射到物理內存中,而且近期被訪問過,處于活躍狀態(tài)匣摘。
  2. 非活躍內存頁(inactive pages)- 這種內存頁已經被映射到物理內存中,但是近期沒有被訪問過炉抒。
  3. 可用的內存頁(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)化泌射,這就是接下來要做的事情粘姜。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市熔酷,隨后出現(xiàn)的幾起案子孤紧,更是在濱河造成了極大的恐慌,老刑警劉巖拒秘,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件号显,死亡現(xiàn)場離奇詭異,居然都是意外死亡躺酒,警方通過查閱死者的電腦和手機押蚤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來羹应,“玉大人揽碘,你說我怎么就攤上這事≡捌ィ” “怎么了雳刺?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長裸违。 經常有香客問我掖桦,道長,這世上最難降的妖魔是什么供汛? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任滞详,我火速辦了婚禮,結果婚禮上紊馏,老公的妹妹穿的比我還像新娘料饥。我一直安慰自己,他們只是感情好朱监,可當我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布岸啡。 她就那樣靜靜地躺著,像睡著了一般赫编。 火紅的嫁衣襯著肌膚如雪巡蘸。 梳的紋絲不亂的頭發(fā)上奋隶,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天,我揣著相機與錄音悦荒,去河邊找鬼唯欣。 笑死,一個胖子當著我的面吹牛搬味,可吹牛的內容都是我干的境氢。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼碰纬,長吁一口氣:“原來是場噩夢啊……” “哼萍聊!你這毒婦竟也來了?” 一聲冷哼從身側響起悦析,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤寿桨,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后强戴,有當地人在樹林里發(fā)現(xiàn)了一具尸體亭螟,經...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年骑歹,在試婚紗的時候發(fā)現(xiàn)自己被綠了预烙。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡陵刹,死狀恐怖默伍,靈堂內的尸體忽然破棺而出欢嘿,到底是詐尸還是另有隱情衰琐,我是刑警寧澤,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布炼蹦,位于F島的核電站羡宙,受9級特大地震影響,放射性物質發(fā)生泄漏掐隐。R本人自食惡果不足惜狗热,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望虑省。 院中可真熱鬧匿刮,春花似錦、人聲如沸探颈。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽伪节。三九已至光羞,卻和暖如春绩鸣,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背纱兑。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工呀闻, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人潜慎。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓捡多,卻偏偏與公主長得像,于是被迫代替她去往敵國和親勘纯。 傳聞我的和親對象是個殘疾皇子局服,可洞房花燭夜當晚...
    茶點故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內容