OS X 和 iOS 內(nèi)存管理

物理內(nèi)存

一個(gè)設(shè)備的 RAM 大小尝抖。Mac的RAM大小不固定,用戶可以隨便擴(kuò)展迅皇。而iphone是固定不變的昧辽,以下是維基百科上的資料:

簡單來說,iPhone 8(不包括 plus) 和 iPhone 7(不包括 plus)及之前都是 2G 內(nèi)存登颓,iPhone 6 和 6 plus 及之前都是 1G 內(nèi)存搅荞。

虛擬內(nèi)存

虛擬內(nèi)存是計(jì)算機(jī)系統(tǒng)內(nèi)存管理的一種技術(shù)。它使得應(yīng)用程序認(rèn)為它擁有連續(xù)的可用的內(nèi)存(一個(gè)連續(xù)完整的地址空間)框咙,而實(shí)際上咕痛,它通常是被分隔成多個(gè)物理內(nèi)存碎片,還有部分暫時(shí)存儲(chǔ)在外部磁盤存儲(chǔ)器上喇嘱,在需要時(shí)進(jìn)行數(shù)據(jù)交換 (iOS并無數(shù)據(jù)交換)茉贡。

從系統(tǒng)角度來說,每個(gè)進(jìn)程都有一個(gè)自己私有的相同大小的虛擬內(nèi)存空間者铜。虛擬內(nèi)存空間大小跟內(nèi)存地址表示位數(shù)有關(guān)(也就是指針的位數(shù))腔丧,32位系統(tǒng)下為32位, 64位系統(tǒng)下為64位。所以32位系統(tǒng)的虛擬內(nèi)存是4GB作烟,而64位是 18EB(1EB = 1000PB, 1PB = 1000TB)愉粤。

操作系統(tǒng)的位數(shù) >= CPU的位數(shù),比如64位CPU上可以運(yùn)行32位系統(tǒng)拿撩,只是沒有充分利用CPU的運(yùn)算能力衣厘。 而ios系統(tǒng)一般跟CPU保持位數(shù)一致,而iphone 5s(A7)及以后的CPU都是64位的压恒。

系統(tǒng)將虛擬內(nèi)存和物理內(nèi)存分割成統(tǒng)一大小的單元头滔,叫做頁(page)。在 OS X 和早期的iOS里涎显,頁大小均為4K坤检;之后基于A7和A8的iOS里,采用虛擬內(nèi)存每頁16K期吓,物理內(nèi)存每頁4K早歇;基于A9或更新CPU的iOS里倾芝,頁大小均為16K.

CPU有個(gè)內(nèi)存管理單元(MMU), 它維護(hù)了一張頁表(page table), 可以將虛擬地址映射到物理地址。用戶訪問虛擬地址時(shí)箭跳,會(huì)自動(dòng)被MMU轉(zhuǎn)換成物理地址晨另。當(dāng)CPU訪問的虛擬地址并未映射到物理地址時(shí),CPU會(huì)觸發(fā)頁錯(cuò)誤(page fault)中斷, 并暫停當(dāng)前執(zhí)行的程序代碼谱姓,然后分配一塊干凈的物理內(nèi)存借尿,從磁盤中加載所需的一頁數(shù)據(jù)到該物理內(nèi)存,同時(shí)更新頁表屉来,然后繼續(xù)執(zhí)行程序代碼路翻。

當(dāng)進(jìn)程向系統(tǒng)申請內(nèi)存時(shí),系統(tǒng)也并不會(huì)直接返回物理內(nèi)存的地址茄靠,而是返回一個(gè)虛擬內(nèi)存地址茂契。僅當(dāng)CPU需要訪問該虛擬內(nèi)存地址時(shí),系統(tǒng)才會(huì)分配并映射到物理內(nèi)存慨绳。

image.png

VM Object

進(jìn)程的虛擬地址空間包含了多個(gè)區(qū)域(VM Region), 每個(gè)VM Region管理一段連續(xù)的虛擬內(nèi)存頁掉冶,這些頁擁有相同的屬性(如讀寫權(quán)限、是否是 wired脐雪,也就是是否能被 page out)厌小。舉幾個(gè)例子:

  • mapped file,即映射到磁盤的一個(gè)文件
  • __TEXT战秋,r-x召锈,多數(shù)為二進(jìn)制
  • __DATA,rw-获询,為可讀寫數(shù)據(jù)
  • MALLOC_(SIZE)涨岁,顧名思義是 malloc 申請的內(nèi)存

每個(gè) VM Region 對應(yīng)一個(gè)內(nèi)核數(shù)據(jù)結(jié)構(gòu),名為 VM Object吉嚣。Object 會(huì)記錄這個(gè) Region 內(nèi)存的屬性:

  • Resident pages - 已經(jīng)被映射到物理內(nèi)存的虛擬內(nèi)存頁列表
  • Size - 所有內(nèi)存頁所占區(qū)域的大小
  • Pager - 用來處理內(nèi)存頁在硬盤和物理內(nèi)存中交換問題
  • Attributes - 這塊內(nèi)存區(qū)域的屬性梢薪,比如讀寫的權(quán)限控制
  • Shadow - 用作(copy-on-write)寫時(shí)拷貝的優(yōu)化
  • Copy - 用作(copy-on-write)寫時(shí)拷貝的優(yōu)化

VM Object的Pager一般有三種類型:

  • default pager 用于磁盤交換內(nèi)存;
  • vnode pager 用于將磁盤文件映射到內(nèi)存
  • device pager 將地址映射到非主存的硬件上尝哆,比如PCI內(nèi)存秉撇、幀緩沖內(nèi)存等,很多I/O Kit的調(diào)用背后用了這個(gè)秋泄。

除了以上Pager琐馆, VM Object還可以關(guān)聯(lián)另外一個(gè)VM Object, 通過這種引用方式實(shí)現(xiàn)寫時(shí)復(fù)用恒序。 這種機(jī)制可以允許不同進(jìn)程或同一進(jìn)程里的不同代碼段共享同一內(nèi)存頁瘦麸,直到有一方試圖寫入數(shù)據(jù)時(shí),才會(huì)真正的拷貝內(nèi)存頁歧胁。

內(nèi)核常駐內(nèi)存(Wired Memory)

內(nèi)核常駐內(nèi)存存儲(chǔ)了內(nèi)核代碼和數(shù)據(jù)結(jié)構(gòu)滋饲,不允許換出到磁盤上厉碟。應(yīng)用(Application)、開發(fā)框架(Framework)和其他用戶級(jí)別的軟件無法分配內(nèi)核常駐內(nèi)存屠缭。然而箍鼓,他們可以影響內(nèi)核常駐內(nèi)存的大小。舉個(gè)例子呵曹,一個(gè)而應(yīng)用創(chuàng)建了線程和端口時(shí)款咖,會(huì)隱式的分配常駐內(nèi)存來存放對應(yīng)的內(nèi)核資源铐殃。

  • Process: 16k
  • Thread: blocked in a continuation—5 k; blocked—21 k
  • Mach port: 116b
  • Mapping: 32 b
  • Library: 2k plus 200b for each task that uses it
  • Memory region: 160b

除了應(yīng)用運(yùn)行產(chǎn)生的內(nèi)核常駐內(nèi)存砍聊,內(nèi)核自己本身也會(huì)創(chuàng)建一些常駐內(nèi)存來存儲(chǔ)以下實(shí)體:

  • VM Objects
  • the virtual memory buffer cache
  • I/O buffer caches
  • drivers

內(nèi)核頁表

內(nèi)核維護(hù)了三種狀態(tài)的頁表:

  • 活躍的內(nèi)存頁(active page):內(nèi)存頁已經(jīng)被映射到物理內(nèi)存中背稼,而且近期被訪問過,處于活躍狀態(tài)词疼。
  • 非活躍的內(nèi)存頁(inactive page):內(nèi)存頁已經(jīng)被映射到物理內(nèi)存中俯树,但是近期沒有被訪問過许饿。
  • 可用的內(nèi)存頁(free page):沒有關(guān)聯(lián)到虛擬內(nèi)存頁的物理內(nèi)存頁集合陋率。

如果可用內(nèi)存頁(free page)數(shù)量低于一個(gè)閾值時(shí)(根據(jù)物理內(nèi)存大小定義的)秽晚,系統(tǒng)會(huì)去平衡一下這個(gè)隊(duì)列赴蝇。 具體就是從非活躍內(nèi)存里獲取劲蜻。在OS X里是通過將非活躍頁的數(shù)據(jù)交換到磁盤后先嬉,再將非活躍頁清理并放置到可用內(nèi)存頁中坝初。

Page Out 過程

在OSX上系統(tǒng)會(huì)將不活躍的內(nèi)存塊寫入硬盤鳄袍,一般稱之為Swap outPage out拗小。

由于虛擬內(nèi)存的空間遠(yuǎn)遠(yuǎn)大于物理內(nèi)存哀九,在任意一個(gè)時(shí)間點(diǎn)阅束,虛擬內(nèi)存中的一個(gè)頁并不一定總是在物理內(nèi)存中息裸,而是可能被暫時(shí)存到了磁盤上呼盆,這樣物理內(nèi)存便可以暫時(shí)釋放這部分空間厨幻,供優(yōu)先級(jí)更高的任務(wù)使用况脆,因此磁盤可以作為 backing store 以擴(kuò)展物理內(nèi)存(MacOS 中有格了,iOS 沒有)笆搓。

具體實(shí)現(xiàn)是满败,內(nèi)核遍歷活躍頁表和非活躍頁表算墨,并執(zhí)行以下操作:

  1. 如果活躍頁表的某頁最近沒有被訪問過净嘀,則將它移入非活躍頁表暑刃。
  2. 如果非活躍頁表的某頁最近被訪問過岩臣,則內(nèi)核會(huì)找出它所在的VM Object.
  3. 如果VM Object沒有關(guān)聯(lián)過pager篙议,那就給他創(chuàng)建一個(gè)default pager
  4. VM Object的default pager嘗試將該頁寫入磁盤交互區(qū)(backing store)
  5. 如果pager完成寫入驯遇,內(nèi)核會(huì)釋放該頁占用的物理內(nèi)存会涎,并將其移入可用頁表里在塔。

Page In 過程

當(dāng)程序代碼試圖訪問某個(gè)還未映射到物理內(nèi)存的虛擬地址時(shí),會(huì)觸發(fā)兩種內(nèi)存訪問錯(cuò)誤(fault):

  • soft fault: 要訪問的頁數(shù)據(jù)在物理內(nèi)存里篱蝇,但并未映射到當(dāng)前進(jìn)程的虛擬地址空間零截。
  • hard fault: 要訪問的頁數(shù)據(jù)并未在物理內(nèi)存里,可能是之前被交換到磁盤了弧哎,也可能是文件的一部分還未映射到內(nèi)存撤嫩。 這也是最經(jīng)典的頁錯(cuò)誤(page fault)

任何一種錯(cuò)誤發(fā)生時(shí)序攘,內(nèi)核會(huì)定位到當(dāng)前region關(guān)聯(lián)的VM Object丈牢,并且遍歷VM Object的常駐內(nèi)存(resident pages)頁表赡麦,如果目標(biāo)頁在該表中泛粹,那么就觸發(fā)soft fault,否則觸發(fā)hard fault晶姊。

針對soft fault,內(nèi)核會(huì)將對應(yīng)的物理頁映射到當(dāng)前進(jìn)程的虛擬地址空間里,并且將該頁標(biāo)記為活躍蒙挑。針對hard fault忆蚀,VM Object的pager將從磁盤backing store或文件中查找對應(yīng)的頁數(shù)據(jù)馋袜,并將其復(fù)制到物理內(nèi)存里,然后加入到活躍頁表里泽台。

Swap In/Out & Page In/Out

  • 磁盤內(nèi)部有一個(gè)區(qū)域叫做交換空間(Swap Space)怀酷,MMU(內(nèi)存管理單元) 會(huì)將暫時(shí)不用的內(nèi)存塊內(nèi)容寫在該交互空間上,這就是Swap Out笔横;當(dāng)需要時(shí)候再從Swap Space中讀取到內(nèi)存中吹缔,這就是Swap In厢塘;Swap in和swap out的操作都是比較耗時(shí)的, 頻繁的Swap in和Swap out操作非常影響系統(tǒng)性能抓半;

  • Page In/Out和 Swap In/Out 概念類似笛求,只不過Page In/Out是將某些頁的數(shù)據(jù)寫到內(nèi)存/從內(nèi)存寫回磁盤交互區(qū);而Swap In/Out是將整個(gè)地址空間的數(shù)據(jù)寫到內(nèi)存/從內(nèi)存寫回磁盤交互區(qū)蜂嗽;本質(zhì)都是交互機(jī)制植旧。

  • macOS支持這類交換機(jī)制界阁,但是iOS不支持泡躯;主要有兩方面考慮:

    1. 移動(dòng)設(shè)備的閃存讀寫次數(shù)有限较剃,頻繁寫會(huì)降低壽命惰拱;
    2. 相比PC機(jī)偿短,移動(dòng)設(shè)備閃存空間有限(15年6s最小存儲(chǔ)空間16GB降传、最大128GB婆排;19年XS Max最小64GB,最大521GB)

Memory Compression

由于閃存容量和讀寫壽命的限制翼悴,iOS 上沒有Disk swap機(jī)制鹦赎,取而代之使用Memory Compression。等壓縮后內(nèi)存也不夠用后陪踩,iOS上則會(huì)通知App,讓App清理內(nèi)存傻谁,也就是我們熟知的Memory Warning

內(nèi)存壓縮技術(shù)是從 OS X Mavericks (10.9) 開始引入的(iOS 則是 iOS 7 開始)态蒂,可以參考官方文檔:OS X Mavericks Core Technology Overview钾恢。該技術(shù)在內(nèi)存緊張時(shí)能夠?qū)⒆罱褂眠^的內(nèi)存占用壓縮至原有大小的一半以下刑桑,并且能夠在需要時(shí)解壓復(fù)用祠斧。它在節(jié)省內(nèi)存的同時(shí)提高了系統(tǒng)的響應(yīng)速度,其特點(diǎn)可以歸結(jié)為:

  • 減少了不活躍內(nèi)存占用
  • 改善電源效率吴超,通過壓縮減少磁盤IO帶來的損耗
  • 壓縮/解壓十分迅速,能夠盡可能減少 CPU 的時(shí)間開銷
  • 支持多核并行操作

這里要注意鸟悴,對于已經(jīng)被壓縮過的內(nèi)存,如果嘗試釋放其中一部分震贵,則會(huì)先將它解壓。而解壓過程帶來的內(nèi)存增大寇甸,可能適得其反, 典型的場景就是內(nèi)存告警時(shí)清理NSDictionary緩存數(shù)據(jù)。對于緩存數(shù)據(jù)或可重建數(shù)據(jù),應(yīng)盡量使用NSCache或NSPurableData偏窝,收到內(nèi)存警告時(shí)伦意,系統(tǒng)自動(dòng)處理內(nèi)存釋放操作,并且是線程安全的离钝。

內(nèi)存類型

Virtual Memory

OS X虛擬內(nèi)存分配大小
Virtual Memory = Dirty Memory + Clean Memory + Swapped Memory
iOS 虛擬內(nèi)存分配大小
Virtual Memory = Dirty Memory + Clean Memory + Compressed Memory

獲取App申請到的所有虛擬內(nèi)存:

- (int64_t)memoryVirtualSize {
    struct task_basic_info info;
    mach_msg_type_number_t size = (sizeof(task_basic_info_data_t) / sizeof(natural_t));
    kern_return_t ret = task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t)&info, &size);
    if (ret != KERN_SUCCESS) {
        return 0;
    }
    return info.virtual_size;
}

Clean Memory

能夠被系統(tǒng)清理出內(nèi)存且在需要時(shí)能重新加載數(shù)據(jù)的Page:

  • 應(yīng)用或系統(tǒng)庫的二進(jìn)制可執(zhí)行文件
  • Memory mapped files:image.jpg,.data,.model 文件等碘橘,這些文件通常是只讀的,映射的內(nèi)存頁可以被系統(tǒng)直接釋放掉错负,需需要的時(shí)候再從文件載入到內(nèi)存。
  • Framework:__DATA_CONST字段识颊,但是有一點(diǎn)需要注意:這個(gè)字段在創(chuàng)建的時(shí)候是 Clean Page 類型的,但是當(dāng)程序運(yùn)行起來的時(shí)候刃跛,如果我們對系統(tǒng)方法進(jìn)行了 Swizzling 就會(huì)把這個(gè)內(nèi)存頁變成 Dirty Page检号。
  • malloc分配后但還未寫過的內(nèi)存(其實(shí)僅分配了虛擬內(nèi)存,真正寫時(shí)才會(huì)分配物理內(nèi)存)凹蜂。

Dirty Memory

主要強(qiáng)調(diào)不可被重復(fù)使用的內(nèi)存,準(zhǔn)確講是如果不交換到硬盤保存狀態(tài)就不能復(fù)用的內(nèi)存(IOS系統(tǒng)根本沒有swap機(jī)制)卿啡。

  • 所有堆上的對象(array,uiview官辽,string等等)。
  • 圖片解析緩沖(CGRasterData,imageIO)俗批。
  • Framework 的__DATA 和 __DATA_DIRTY部分

iOS中的內(nèi)存警告,只會(huì)釋放clean memory干像。因?yàn)閕OS認(rèn)為dirty memory有數(shù)據(jù)驰弄,不能清理麻汰。所以應(yīng)盡量避免dirty memory過大

Clean和Dirty示例
int *array = malloc(20000 * sizeof(int)); // 第1步
array[0] = 32                             // 第2步
array[19999] = 64                         // 第3步
  • 第一步,申請一塊長度為80000 字節(jié)的內(nèi)存空間戚篙,按照一頁 16KB 來計(jì)算五鲫,就需要 6 頁內(nèi)存來存儲(chǔ)。當(dāng)這些內(nèi)存頁開辟出來的時(shí)候臣镣,它們都是 Clean 的辅愿;
  • 第二步智亮,向處于第一頁的內(nèi)存寫入數(shù)據(jù)時(shí)忆某,第一頁內(nèi)存會(huì)變成 Dirty;
  • 第三步阔蛉,當(dāng)向處于最后一頁的內(nèi)存寫入數(shù)據(jù)時(shí)弃舒,這一頁也會(huì)變成 Dirty;

Compressed Memory

注意状原,用vvmap等工具測量的Compressed Memory都是壓縮之前的大小

Resident Memory

已經(jīng)被映射到虛擬內(nèi)存中的物理內(nèi)存聋呢。存在一些“非代碼執(zhí)行開銷”,如系統(tǒng)和應(yīng)用二進(jìn)制加載的內(nèi)存颠区。

這里存在兩種Resident Memory削锰,系統(tǒng)的和我們APP的:
All Resident = 系統(tǒng)Resident + APP Resident
APP Resident = Dirty Memory + Clean Memory that loaded in pysical memory(_TEXT + _OBJC_RO + Other)

截屏2020-07-23 上午11.33.12.png-689.4kB

其中系統(tǒng)Resident主要就是dyld_shared_cache(動(dòng)態(tài)庫共享緩存),所有APP的虛擬內(nèi)存對動(dòng)態(tài)庫的物理內(nèi)存映射都是映射到這塊的毕莱,我們運(yùn)行不同的app查看dyld_shared_cache的地址都是一樣的:

截屏2020-07-23 下午12.26.48.png-361.9kB

獲取App消耗的Resident Memory:

#import <mach/mach.h>
- (int64_t)memoryResidentSize {
    struct task_basic_info info;
    mach_msg_type_number_t size = sizeof(task_basic_info_data_t) / sizeof(natural_t);
    kern_return_t ret = task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t)&info, &size);
    if (ret != KERN_SUCCESS) {
        return 0;
    }
    return info.resident_size;
}

Memory Footprint

App消耗的實(shí)際物理內(nèi)存器贩,具體定義在官方文檔 Minimizing your app’s Memory Footprint 里有說明:

Refers to the total current amount of system memory that is allocated to your app.

經(jīng)過測試,Xcode Debug Navigator朋截、系統(tǒng)活動(dòng)監(jiān)控器蛹稍、footprint 命令工具和代碼中phys_footprint 得到的數(shù)據(jù)是一致的,都表示當(dāng)前App消耗的實(shí)際物理內(nèi)存部服。

內(nèi)核代碼里的注釋

/*
* phys_footprint
*   Physical footprint: This is the sum of:
*     + (internal - alternate_accounting)
*     + (internal_compressed - alternate_accounting_compressed)
*     + iokit_mapped
*     + purgeable_nonvolatile
*     + purgeable_nonvolatile_compressed
*     + page_table
*
* internal
*   The task's anonymous memory, which on iOS is always resident.
*
* internal_compressed
*   Amount of this task's internal memory which is held by the compressor.
*   Such memory is no longer actually resident for the task [i.e., resident in its pmap],
*   and could be either decompressed back into memory, or paged out to storage, depending
*   on our implementation.
*
* iokit_mapped
*   IOKit mappings: The total size of all IOKit mappings in this task, regardless of
    clean/dirty or internal/external state].
*
* alternate_accounting
*   The number of internal dirty pages which are part of IOKit mappings. By definition, these pages
*   are counted in both internal *and* iokit_mapped, so we must subtract them from the total to avoid
*   double counting.
*/

App消耗的實(shí)際物理內(nèi)存唆姐,包括:

  • Dirty Memory
  • Compressed Memory
  • Page Table
  • IOKit used
  • NSCache, Purgeable等

通過footprint命令的輸出可以知道廓八,F(xiàn)ootprint主要是Dirty部分(可以粗略理解二者等價(jià))奉芦,也就是我們可以控制優(yōu)化的部分。注意: Resident Memory 包含了 Memory Footprint剧蹂。

獲取App的Footprint:

#import <mach/mach.h>
- (int64_t)memoryPhysFootprint {
    task_vm_info_data_t vmInfo;
    mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
    kern_return_t ret = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t)&vmInfo, &count);
    if (ret != KERN_SUCCESS) {
        return 0;
    }
    return vmInfo.phys_footprint;
}

XNU中Jetsam判斷內(nèi)存過大声功,使用的也是phys_footprint,而非resident size国夜。

Jetsam: 由于iOS設(shè)備不存在交換區(qū)導(dǎo)致的內(nèi)存受限减噪,所以iOS內(nèi)核不得不把一些優(yōu)先級(jí)不高或者占用內(nèi)存過大的殺掉。

內(nèi)存監(jiān)控工具

除了系統(tǒng)提供的活動(dòng)監(jiān)視器來觀察內(nèi)存车吹,還有很多更強(qiáng)大的工具來定位內(nèi)存問題筹裕。

vm_stat

在 macOS 上我們在終端運(yùn)行 vm_stat 可以看到以下系統(tǒng)整體內(nèi)存信息:

?  darwin-xnu git:(master) vm_stat
Mach Virtual Memory Statistics: (page size of 4096 bytes)
Pages free:                              349761.
Pages active:                           1152796.
Pages inactive:                         1090213.
Pages speculative:                        22734.
Pages throttled:                              0.
Pages wired down:                        979685.
Pages purgeable:                         519551.
"Translation faults":                 300522536.
Pages copy-on-write:                   16414066.
Pages zero filled:                     94760760.
Pages reactivated:                      4424880.
Pages purged:                           4220936.
File-backed pages:                       480042.
Anonymous pages:                        1785701.
Pages stored in compressor:             2062437.
Pages occupied by compressor:            598535.
Decompressions:                         4489891.
Compressions:                          11890969.
Pageins:                                6923471.
Pageouts:                                 38335.
Swapins:                                  87588.
Swapouts:                                432061.

這個(gè)系統(tǒng)命令就是通過 host_statistics64() 獲取的,代碼參考窄驹。

這才是最終的系統(tǒng)內(nèi)存占用情況朝卒,以 byte 為單位:
(active_count + wired_count + speculative_count + compressor_page_count) * page_size

footprint

footprint -p 14022 --wired --swapped

Xcode Navigator

初略展示了真實(shí)的物理內(nèi)存消耗。顏色表明了內(nèi)存占用是否合理乐埠。Xcode Navigator = footprint + 調(diào)試需要抗斤。不跟蹤VM囚企。往往初略觀察App的內(nèi)存占用情況,不能作為精確的參考瑞眼。

Instuments Allocations

這里顯示的內(nèi)存龙宏,其實(shí)只是整個(gè)App占用內(nèi)存的一部分,即開發(fā)者自行分配的內(nèi)存伤疙,如各種類實(shí)例等银酗。

  • 主要是MALLOC_XXX, VM Region, 以及部分App進(jìn)程創(chuàng)建的VM Region。
  • 非動(dòng)態(tài)的內(nèi)存徒像,及部分其他動(dòng)態(tài)庫創(chuàng)建的VM Region并不在Allocations的統(tǒng)計(jì)范圍內(nèi)黍特。
  • 主程序或動(dòng)態(tài)庫的_DATA數(shù)據(jù)段、Stack函數(shù)棧锯蛀,并非通過malloc分配灭衷,因此不在Allocations統(tǒng)計(jì)內(nèi)。
All Heap Allocations:
  • malloc
  • CFData
All Anonymous VM

無法由開發(fā)者直接控制旁涤,一般由系統(tǒng)接口調(diào)用申請的翔曲。例如圖片之類的大內(nèi)存,屬于All Anonymous VM -> VM: ImageIO_IOSurface_Data拭抬,其他的還有IOAccelerator與IOSurface等跟GPU關(guān)系比較密切的.

Instruments VM Tracker

Instruments里打開Allocations就可以看到有VM Tracker了部默,記得要開啟自動(dòng)捕獲快照后才能展示結(jié)果

image

上面是一個(gè)空的iOS App的VM Tracker示意圖。一共有9列造虎,下面我來一一解釋它們的含義傅蹂。

  • % of Res, 當(dāng)前Type的VM Regions總Resident Size占比算凿。
  • Type份蝴,VM Regions的Type,AllDirty算是統(tǒng)計(jì)性質(zhì)的Type氓轰,__TEXT表示代碼段的內(nèi)存映射婚夫,__DATA表示數(shù)據(jù)段的內(nèi)存映射。
  • # Regs署鸡,當(dāng)前Type的VM Region總數(shù)案糙。
  • Path,VM Region是從哪個(gè)文件映射過來靴庆,因?yàn)橛行╊愃朴赺_DATA和mapped file的內(nèi)存塊是從文件直接映射過來的时捌。
  • Resident Size,使用的物理內(nèi)存量炉抒。
  • Dirty Size奢讨,使用中的物理內(nèi)存塊如果不交換到硬盤保存狀態(tài)就不能復(fù)用,那么就是Dirty的內(nèi)存塊焰薄。
  • Swapped Size, 在OSX中拿诸,不活躍的內(nèi)存頁可以被交換到硬盤扒袖,這是被交換的大小。在iOS中亩码,只有非Dirty的內(nèi)存頁可以被交換季率,或者說是被卸載。
  • Virtual Size蟀伸,VM Regions所占虛擬內(nèi)存的大小
  • Res. %蚀同,Resident Size在Virtual Size中的占比

Xcode Memory Debugger

該工具可以非常方便地查看所有對象的內(nèi)存使用情況缅刽、依賴關(guān)系啊掏,以及循環(huán)引用等。如果將其導(dǎo)出為memgraph文件衰猛,也可以使用一些命令來進(jìn)行分析:

vmmap

vmmap memory-info.memgraph

  • 查看摘要
    vmmap --summary memory-info.memgraph
    結(jié)合shell中的grep迟蜜、awk等命令,可以獲得任何想要的內(nèi)存數(shù)據(jù)啡省。
    摘要里的字段含義跟VM Tracker里一致:

  • 查看所有dylib的Dirty Pages的總和
    vmmap -pages memory-info.memgraph | grep '.dylib' | awk '{sum += $6} END { print "Total Dirty Pages:"sum}'

  • 查看CG image相關(guān)的內(nèi)存數(shù)據(jù)
    vmmap memory-info.memgraph | grep 'CG image'

heap

查看堆內(nèi)存

  • 查看Heap上的所有對象
    heap memory-info.memgraph

  • 按照內(nèi)存大小來排序
    heap memory-info.memgraph -sortBySize

  • 查看某個(gè)類的所有實(shí)例對象的內(nèi)存地址
    heap memory-info.memgraph -addresses all | 'MyDataObject'

  • 代碼中通過malloc_size函數(shù)獲取某對象占用的內(nèi)存大小

malloc_size((__bridge const void *)(object))

leaks

  • 查看是否有內(nèi)存泄漏
    leaks memory-info.memgraph
  • 查看內(nèi)存地址處的泄漏情況
    leaks --traceTree [內(nèi)存地址] memory-info.memgraph

lmalloc_history

需要開啟Run->Diagnostics中的Malloc Stack功能娜睛,建議使用Live Allocations Only。則lldb會(huì)記錄debug過程中的對象創(chuàng)建的堆棧卦睹,配合malloc_history畦戒,即可定位對象的創(chuàng)建過程。
malloc_history memory-info.memgraph [address]
malloc_history memory-info.memgraph --fullStacks [address]

內(nèi)存分配實(shí)例

測試環(huán)境:
iPhone11模擬器+iOS14
macOS Catalina 10.15.7
Xcode 12.1

  • malloc 10M內(nèi)存
     void *memBlock = malloc(10 * 1024 * 1024);
內(nèi)存類型 初始大小 當(dāng)前大小 增量
Virtual 5364.49M 5374.50M +10.01M
Resident 119.25M 119.27M +0.02M
Footprint 18.64M 18.65M +0.01M

僅VM增大10M结序,即只分配了虛擬地址障斋,并映射到物理地址

  • memset 10M內(nèi)存
    memset(memBlock, 0, 10 * 1024 * 1024);
內(nèi)存類型 初始大小 當(dāng)前大小 增量
Virtual 5374.50M 5374.50M 0
Resident 119.27M 129.27M +10M
Footprint 18.65M 28.65M +10M

訪問虛擬地址,會(huì)觸發(fā)分配物理內(nèi)存頁徐鹤。Resident和Footprint都增加垃环。

  • 分配10M虛擬內(nèi)存
  vm_address_t address;
  vm_size_t size = 100*1024*1024;
  vm_allocate((vm_map_t)mach_task_self(), &address, size, VM_MAKE_TAG(200) | VM_FLAGS_ANYWHERE);
內(nèi)存類型 初始大小 當(dāng)前大小 增量
Virtual 5610.42M 5620.42M +10M
Resident 119.16M 119.16M 0
Footprint 18.66M 18.66M 0
  • imageWithFile 加載7.2M圖片
    圖片分辨率3502?×?1518,含透明通道返敬,解碼后的Bitmap大小應(yīng)該為20.279M
 NSURL* imageUrl = [[NSBundle mainBundle] URLForResource:@"big_pic" withExtension:@"png"];
 UIImage* image = [UIImage imageWithContentsOfFile:imageUrl.path];
內(nèi)存類型 初始大小 當(dāng)前大小 增量
Virtual 5661.55M 5661.55M 0
Resident 121.67M 122.96M +1.29M
Footprint 17.68M 17.76M +0.08M

Resident增加了1M左右遂庄,而不是7.2M,因?yàn)槲募成鋬?nèi)存是Clean Memory劲赠,沒必要完全加載到物理內(nèi)存涛目。

  • imageWithFile渲染上屏
    self.imageView.image = image;
    //延遲1s后統(tǒng)計(jì)內(nèi)存,保證渲染結(jié)束
內(nèi)存類型 初始大小 當(dāng)前大小 增量
Virtual 5661.55M 5830.41M + 40.22M
Resident 122.96M 164.43M +41.47M
Footprint 17.76M 58.87M +41.11M

Footprint增加的大小約為圖片解碼后的大小的兩倍凛澎,后面會(huì)分析霹肝。

  • imageWithFile的Footprint分析
初始化狀態(tài)
加載并展示圖片后
退出頁面后

分析:
圖片渲染時(shí),主要增加了兩個(gè)跟解碼大小接近的Dirty內(nèi)存:

 20 MB        0 B      1408 KB         26    CoreAnimation
 20 MB        0 B          0 B          2    MALLOC_LARGE

所以Resident和Footprint(footprint命令統(tǒng)計(jì))會(huì)增加40M预厌。
退出界面后阿迈,CoreAnimation占用內(nèi)存被釋放, 而MALLOC_LARGE還存在。
而Instruments的Allocations只捕獲CoreAnimation的分配轧叽,說明MALLOC_LARGE是內(nèi)核分配的苗沧,不在Allocations統(tǒng)計(jì)范圍

Allocations

但換了個(gè)測試環(huán)境刊棕,結(jié)果卻不一樣了:MALLOC_LARGE不再增加20M,而是MALLOC_LARGE_REUSABLE增加了20M的Clean Memory待逞。所以猜想MALLOC_LARGE 也是系統(tǒng)為了復(fù)用而預(yù)分配的內(nèi)存甥角,只是由Clean Memory換成了Dirty Memory,具體意義未知识樱。

iPhone11模擬器+iOS13.3
macOS Catalina 10.15.3
Xcode 11.3

  Dirty      Clean  Reclaimable    Regions    Category

656 KB        0 B          0 B          9    MALLOC_LARGE
0 B           0 B        20 MB          1    MALLOC_LARGE_REUSABLE
  • imageNamed加載7.2M圖片(相同圖片嗤无,不同名字)
    UIImage* image = [UIImage imageNamed:@"big_pic2"];
內(nèi)存類型 初始大小 當(dāng)前大小 增量
Virtual 5679.04M 5679.04M 0
Resident 122.01M 123.36M +0.04M
Footprint 17.91M 18.01M +0.01M
  • imageNamed渲染上屏
    self.imageView2.image = image;
    //延遲1s后統(tǒng)計(jì)內(nèi)存,保證渲染結(jié)束
內(nèi)存類型 初始大小 當(dāng)前大小 增量
Virtual 5679.04M 5868.07M + 40.59M
Resident 123.36M 184.90M +40.7M
Footprint 18.01M 58.91M +20.35M
  • imageNamed的Footprint分析
初始化
加載并上屏
退出界面

分析:
圖片渲染時(shí)怜庸,與ImageWithFile的消耗類似当犯,增加了40M Dirty內(nèi)存,但還多了ImageIO產(chǎn)生的20M可回收內(nèi)存(Reclaimable割疾,不計(jì)算到Footprint里)嚎卫,多的這塊其實(shí)是ImageName自動(dòng)解碼產(chǎn)生的:

  Dirty      Clean  Reclaimable    Regions    Category
    ---        ---          ---        ---    ---
  20 MB        0 B      1408 KB         26    CoreAnimation
  20 MB        0 B          0 B          2    MALLOC_LARGE
    0 B        0 B        20 MB          1    ImageIO

退出界面時(shí),F(xiàn)ootprint不降反升宏榕,CoreAnimation多消耗了2M拓诸。但再次進(jìn)入同一個(gè)測試頁面,CoreAnimation的消耗回到了20M麻昼〉熘В可見imageNamed會(huì)將解碼的數(shù)據(jù)緩存在CoreAnimation里,如果圖片較大還是建議使用imageWithFile

參考文章

About the Virtual Memory System
iOS Memory Deep Dive
macOS 內(nèi)核之內(nèi)存占用信息
關(guān)于iOS內(nèi)存的深入排查和優(yōu)化
iOS內(nèi)存分配:虛擬內(nèi)存
iOS 內(nèi)存管理研究
Tips for Allocating Memory
Image and Graphics Best Practices

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末抚芦,一起剝皮案震驚了整個(gè)濱河市倍谜,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌燕垃,老刑警劉巖枢劝,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異卜壕,居然都是意外死亡您旁,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門轴捎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鹤盒,“玉大人,你說我怎么就攤上這事侦副≌炀猓” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵秦驯,是天一觀的道長尺碰。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么亲桥? 我笑而不...
    開封第一講書人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任洛心,我火速辦了婚禮,結(jié)果婚禮上题篷,老公的妹妹穿的比我還像新娘词身。我一直安慰自己,他們只是感情好番枚,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開白布法严。 她就那樣靜靜地躺著,像睡著了一般葫笼。 火紅的嫁衣襯著肌膚如雪深啤。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,144評(píng)論 1 285
  • 那天渔欢,我揣著相機(jī)與錄音墓塌,去河邊找鬼。 笑死奥额,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的访诱。 我是一名探鬼主播垫挨,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼触菜!你這毒婦竟也來了九榔?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬榮一對情侶失蹤涡相,失蹤者是張志新(化名)和其女友劉穎哲泊,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體催蝗,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡切威,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了丙号。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片先朦。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖犬缨,靈堂內(nèi)的尸體忽然破棺而出喳魏,到底是詐尸還是另有隱情,我是刑警寧澤怀薛,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布刺彩,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏创倔。R本人自食惡果不足惜三热,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望三幻。 院中可真熱鬧就漾,春花似錦、人聲如沸念搬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽朗徊。三九已至首妖,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間爷恳,已是汗流浹背有缆。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留温亲,地道東北人棚壁。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像栈虚,于是被迫代替她去往敵國和親袖外。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容