0. iOS內存基本原理
在接觸iOS開發的時候,我們都知道“引用計數”的概念,也知道ARC和MRR,但其實這僅僅是對堆內存上對象的內存管理。用WWDC某Session里的話說,這其實只是內存管理的冰山一角。
在內存管理方面,其實iOS和其它操作系統總體上來說是大同小異的,大的框架原理基本相似,小的細節有所創新和不同。
和其它操作系統上運行的進程類似,iOS App進程的地址空間也分為代碼區、數據區、棧區和堆區等。進程開始時,會把mach-o文件中的各部分,按需加載到內存當中。
而對於一般的iPhone,實際物理內存都在1G左右,對於超大的內存需求怎么辦呢?其實這也是和其它操作系統一樣的道理,都由系統內核維護一套虛擬內存系統。但這里需要注意的是iOS的虛存系統原則略有不同,最截然不同的地方就是當物理內存緊張情況時的處理。
當物理內存緊張時,iOS會把可以通過重新映射來加載的內容直接清理出內存,對於不可再生的數據,iOS需要App進程配合處理,向各進程發送內存警告要求配合釋放內存。對於不能及時釋放足夠內存的,直接Kill掉進程,必要時時甚至是前台運行的App。
如上所述,iOS在外存沒有交換區,沒有內存頁換出的過程。
1. malloc基本原理
在iOS App進程地址空間的各個區域中,最靈活的就要屬堆區了,它為進程動態分配內存,也是我們經常和內存打交道的地方。
通常,我們會在需要新對象的時候,進行 [NSObject alloc]調用,而釋放對象時需要release(ARC會自動幫你做到這些)。
而這些alloc、release方法的調用,通常最終都會走到libsystem_malloc.dylib的malloc()和free()函數這里。libsystem_malloc.dylib是iOS內核之外的一個內存庫,我們App進程需要的內存,先回請求到這里,但最終libsystem_malloc.dylib也都會向iOS的系統內核發起申請,映射實際內存到App進程的地址空間上。
從蘋果公開的malloc源碼上來看,malloc的原理大致如下:
malloc內存分配基於malloc zone,並將內存分配按大小分為nano、tiny、small、large幾種類型,申請時按需進行最適分配
malloc在首次調用時,初始化default zone,在64位情況下,會初始化default zone為nano zone,同時初始化一個scalable zone作為helper zone,nano zone負責nano大小的分配,scalable zone則負責tiny、small和large內存的分配
每次malloc時,根據傳入的size參數,優先交給nano zone做分配處理,如果大小不在nano范圍,則轉交給helper zone處理。
(截圖自http://www.tinylab.org/memory-allocation-mystery-%C2%B7-malloc-in-os-x-ios/)
下面分別對nano zone和scalable zone上分配內存的源碼做簡要解讀(由於蘋果Open source的代碼是針對OS X的特定版本,具體細節可能與iOS上有所不同,如地址空間分布)。
2. nano malloc
在支持64位的條件按下,malloc優先考慮nano malloc,負責對256B以下小內存分配,單位是16B。
nano zone分配內存的地址空間范圍是0x00006nnnnnnnnnnn(OSX上64位情況),將地址空間從大到小一次分為Magazine、Band和Slot幾個級別。
- Magazine范圍對應於CPU,CPU0對應Mag0,CPU1對應Mag1,依次類推;
- Band范圍為2M,連續內存分配當內存不夠時以Band為單位向內核請求;
- Slot則對應於每個Band中128K大小的范圍,每個Band都分為16個Slot,分別對應於16B、32B、...256B大小,支持它們的內存分配
分配過程:
- 確定當前cpu對應的mag和通過size參數計算出來的slot,去對應metadata的鏈表中取已經被釋放過的內存區塊緩存,如果取到檢查指針地址是否有問題,沒有問題就直接返回;
- 初次進行nano malloc時,nano zone並沒有緩存,會直接在nano zone范圍的地址空間上直接分配連續地址內存;
- 如當前Band中當前Slot耗盡則向系統申請新的Band(每個Band固定大小2M,容納了16個128k的槽),連續地址分配內存的基地址、limit地址以及當前分配到的地址由meta data結構維護起來,而這些meta data則以Mag、Slot為維度(Mag個數是處理器個數,Slot是16個)的二維數組形式,放在nanozone_t的meta_data字段中。
當App通過free()釋放內存時:malloc庫會檢查指針地址,如果沒有問題,則以鏈表的形式將這些區塊按大小存儲起來。這些鏈表的頭部放在meta_data數組中對應的[mag][slot]元素中。
其實從緩存獲取空余內存和釋放內存時都會對指向這篇內存區域的指針進行檢查,如果有類似地址不對齊、未釋放/多次釋放、所屬地址與預期的mag、slot不匹配等情況都會以報錯結束。
下圖是我根據個人理解梳理出來的一個關系圖,圖中標出了nanozone_t、meta_data_t等相關結構的關鍵字段畫了出來(OSX)。
除了分配和釋放,系統內存吃緊時,nano zone需將cache的內存區塊還給系統,這主要是通過對各個slot對應的meta data上掛着的空閑鏈表上內存區塊回收來完成。
3. scalable zone上內存分配簡要分析
對於超出nano大小范圍或者不支持nano分配的,直接會在scalable zone(下文簡稱szone)上分配內存。由於szone上的內存分配比起nano分配要較為復雜,細節繁多,下面僅作簡要介紹,感興趣的同學可以直接閱讀源碼。
在szone上分配的內存包括tiny、small和large三大類,其中tiny和small的分配、釋放過程大致相同,large類型有自己的方式管理。
而tiny、small的方式也依然遵循nano分配中的原則,新內存從系統申請並分配,free后按照大小以特定的形式緩存起來,供后續分配使用。這里的分配在region上進行,region和nano malloc里的band概念極為相似,但不同的是地址空間未必連續,而且每個region都有自己的位圖等描述信息。和nano,一樣每個cpu有一個magazine,除此之外還分配了一個index為-1的magazine作為后備之用。
下面是一個簡圖。
以tiny的情況為例,分配時:
- 確定當前線程所在處理器的magazine index,找到對應的magazine結構。
- 優先查看上次最后釋放的區塊是否和此次請求的大小剛好相等(都是對齊之后的slot大小),如果是則直接返回。
- 如果不是,則查找free list中當前請求大小區塊的空閑緩存列表,如果有返回,並整理列表。
- 如果沒有,則在free list找比當前申請區塊大的,而且最接近的緩存,如果有返回,並把剩余大小放到free list中另外的鏈表上。(這里需要注意的是,在一般情況下,free list分為64個槽,0-62上掛載區塊的大小都是按16B為單位遞增,63為所有更大的內存區塊掛載的地方)
- 上面幾項都不行,就在最后一個region的尾部或者首部(如果支持內部ALSR)找空閑區域分配。
- 如果還是不行,說明所有現有region都沒空間可用了,那么從一個后備magazine中取出一個可用region,完整地拿過來放到當前magazine,再走一遍上面的步驟。
- 如果這都不成,那只能向內核申請一塊新的region區域,掛載到當前的magazine下並分配內存。
- 要是再不行就沒招了,系統也給不到內存,就報錯返回。
free時:
- 檢查指針指向地址是否有問題。
- 如果last free指針上沒有掛載內存區塊,則放到last free上就OK了。
- 如果有last free,置換內存,並把last free原有內存區塊掛載到free list上(在掛載的free list前,會先根據region位圖檢查前后區塊是否能合並成更大區塊,如果能會合並成一個)。
- 合並后所在的region如果空閑字節超過一定條件,則將把此region放到后備的magazine中(-1)。
- 如果整個region都是空的,則直接還給系統內核,一了百了。
而large的情況,malloc以頁為單位申請和分配內存,不區分magazine,szone統一維護一個hash table管理已申請的內存。而且由於內存區域都比較龐大,只緩存總量2G的區塊,分為16個元素,每個最大為128M。large相關的結構相對簡單,就不特意畫圖了。
綜上,iOS內存管理和malloc庫的源碼整理到此。如果發現分析得有紕繆的地方或者描述不完整的請不吝指出,歡迎隨時交流。