概述: 結(jié)合內(nèi)存分配和使用內(nèi)存訪問再談內(nèi)存管理(不適合內(nèi)存管理小白)
一.為什么要管理內(nèi)存?
-
1.1 answer:
系統(tǒng)不幫我們管理,我們就自己管理啊!!!,聽起來很對啊,可是系統(tǒng)為什么要提我們管理呢? 當(dāng)然是為了以最優(yōu)方式最大化利用有限的內(nèi)存資源了!,那就有必要先來了解下內(nèi)存分配算法了.
-
1.2內(nèi)存分配算法
(不想看的直接看后面總結(jié))
-
首次適應(yīng)算法
使用該算法進(jìn)行內(nèi)存分配時(shí),從空閑分區(qū)鏈?zhǔn)组_始查找,直至找到一個(gè)能滿足其大小需求的空閑分區(qū)為止。然后再按照作業(yè)的大小,從該分區(qū)中劃出一塊內(nèi)存分配給請求者,余下的空閑分區(qū)仍留在空閑分區(qū)鏈中。
該算法傾向于使用內(nèi)存中低地址部分的空閑分區(qū),在高地址部分的空閑分區(qū)非常少被利用,從而保留了高地址部分的大空閑區(qū)。顯然為以后到達(dá)的大作業(yè)分配大的內(nèi)存空間創(chuàng)造了條件。缺點(diǎn)在于低址部分不斷被劃分,留下許多難以利用、非常小的空閑區(qū),而每次查找又都從低址部分開始,這無疑會(huì)增加查找的開銷。 -
循環(huán)首次適應(yīng)算法
該算法是由首次適應(yīng)算法演變而成的。在為進(jìn)程分配內(nèi)存空間時(shí),不再每次從鏈?zhǔn)组_始查找,而是從上次找到的空閑分區(qū)開始查找,直至找到一個(gè)能滿足需求的空閑分區(qū),并從中劃出一塊來分給作業(yè)。該算法能使空閑中的內(nèi)存分區(qū)分布得更加均勻,但將會(huì)缺乏大的空閑分區(qū)。
-
最佳適應(yīng)算法
該算法總是把既能滿足需求,又是最小的空閑分區(qū)分配給作業(yè)。為了加速查找,該算法需求將所有的空閑區(qū)按其大小排序后,以遞增順序形成一個(gè)空白鏈。這樣每次找到的第一個(gè)滿足需求的空閑區(qū),必然是最優(yōu)的。孤立地看,該算法似乎是最優(yōu)的,但事實(shí)上并不一定。因?yàn)槊看畏峙浜笫S嗟目臻g一定是最小的,在存儲(chǔ)器中將留下許多難以利用的小空閑區(qū)。同時(shí)每次分配后必須重新排序,這也帶來了一定的開銷。
-
最差適應(yīng)算法
最差適應(yīng)算法中,該算法按大小遞減的順序形成空閑區(qū)鏈,分配時(shí)直接從空閑區(qū)鏈的第一個(gè)空閑分區(qū)中分配(不能滿足需要?jiǎng)t不分配)。非常顯然,如果第一個(gè)空閑分區(qū)不能滿足,那么再?zèng)]有空閑分區(qū)能滿足需要。這種分配方法初看起來不太合理,但他也有非常強(qiáng)的直觀吸引力:在大空閑區(qū)中放入程式后,剩下的空閑區(qū)常常也非常大,于是還能裝下一個(gè)較大的新程式。
最壞適應(yīng)算法和最佳適應(yīng)算法的排序正好相反,他的隊(duì)列指針總是指向最大的空閑區(qū),在進(jìn)行分配時(shí),總是從最大的空閑區(qū)開始查尋。
該算法克服了最佳適應(yīng)算法留下的許多小的碎片的不足,但保留大的空閑區(qū)的可能性減小了,而且空閑區(qū)回收也和最佳適應(yīng)算法相同復(fù)雜。
總結(jié):綜合以上四種常見的內(nèi)存分配算法,我們不看看出,要解決最大的問題就是如何最大化最快捷的利用內(nèi)存,也就是我們內(nèi)存管理的終極目的;
二. 內(nèi)存訪問方式:
簡單說,CPU 就是通過內(nèi)存的物理地址來確定數(shù)據(jù)所在的準(zhǔn)確位置,然后進(jìn)行讀寫操作;
(下面一段內(nèi)容可直接跳過,主要是對上述訪問過程的詳細(xì)分析)
-
2.1底層的訪問流程如下:
- CPU 通過地址總線 確認(rèn)要訪問的內(nèi)存單元號和具體的數(shù)據(jù)地址;
- CPU 通過控制總線告訴內(nèi)存要進(jìn)行的操作,read , write or other;
- CPU與內(nèi)存通過數(shù)據(jù)總線進(jìn)行數(shù)據(jù)傳輸;
以上所有的數(shù)據(jù)傳輸,皆是以高(1)低(0)電平的形式進(jìn)行,是不是已經(jīng)很接近機(jī)器語言了?(很久沒看硬件層的東西了,若有錯(cuò)誤還望指正)
-
2.2為什么要引入物理地址
物理地址怎么來的呢?當(dāng)然是邏輯地址映射而來的,為什么要引入邏輯地址?直接使用物理地址不是更好的節(jié)約CPU開銷嗎?下面來解釋為什么:
假設(shè)一臺(tái)設(shè)備有2G內(nèi)存,還要跑多個(gè)程序,還要接受消耗很大內(nèi)存的程序,他是怎么做到的?搞計(jì)算機(jī)的人都是很聰明的(請?jiān)试S我講一個(gè)事實(shí)),在操作系統(tǒng)層面做了物理地址和邏輯地址之間的映射轉(zhuǎn)換,當(dāng)然處理器硬件上也做了支持。一個(gè)程序在運(yùn)行時(shí),實(shí)際要用到的指令和數(shù)據(jù)都是很有限的,不可能從頭到尾同時(shí)用。那么對于一個(gè)程序來說,假裝自己有非常大的空間,實(shí)際上只要有條理的把暫時(shí)要用到的部分放進(jìn)物理內(nèi)存供CPU訪問就好。那既然每個(gè)程序(進(jìn)程)只用一小塊,那整個(gè)物理內(nèi)存就可以分給多個(gè)程序(進(jìn)程)用了。當(dāng)然,這樣做的前提是,數(shù)據(jù)和指令的動(dòng)態(tài)進(jìn)出,用完了的暫時(shí)不用的踢出內(nèi)存,需要用的及時(shí)加載進(jìn)來。很多實(shí)現(xiàn)方式是在外存中開了個(gè)交換區(qū)供換入換出,但iOS可略有不同.
-
2.3 iOS 是如何應(yīng)對內(nèi)存不足的的
首先,iOS和其它系統(tǒng)一樣,內(nèi)存分頁,每頁4K。多個(gè)頁構(gòu)成一個(gè)region統(tǒng)一管理,負(fù)責(zé)管理的對象是VM object,其中包含了pager、size、resident pages等諸多屬性。
但是 iOS的操作系統(tǒng)引入了沙盒機(jī)制,更是拋棄了不必要的復(fù)雜,在系統(tǒng)層面不支持App內(nèi)存頁換出。當(dāng)內(nèi)存吃緊時(shí),對于可以重新載入的只讀數(shù)據(jù)來說,直接清理掉,而對于可寫的數(shù)據(jù),只能通過App自己去管理維護(hù)。內(nèi)存緊張時(shí),iOS會(huì)向App發(fā)起memory warning,不配合釋放足夠內(nèi)存者,殺!所以,iOS 始終保持著系統(tǒng)自我運(yùn)行環(huán)境良好,并不十分關(guān)心具體應(yīng)用哦;
開發(fā)中注意點(diǎn)1:memory warning 處理不當(dāng), APP 存在被被系統(tǒng)強(qiáng)制殺死的可能性
三. 內(nèi)存分布:
想要管理內(nèi)存,總得知道你要管理的數(shù)據(jù)在哪吧?看一個(gè)經(jīng)典的C 內(nèi)存分布:

最簡單來說分為兩大部分:指令+數(shù)據(jù)。再細(xì)分一點(diǎn),五部分:代碼(機(jī)器碼,看構(gòu)造應(yīng)該是ARM 指令集),初始化數(shù)據(jù)區(qū),未初始化數(shù)據(jù)區(qū),堆,棧。
- 代碼:(指令即機(jī)器碼,看構(gòu)造應(yīng)該是ARM 指令集)就不用說了,最靜態(tài)的,就是只讀的東西;
- 初始化數(shù)據(jù),簡單理解就是有初始值的變量、常量;
- 未初始化數(shù)據(jù),只聲明未給值的變量,運(yùn)行前統(tǒng)統(tǒng)為0,之所以單獨(dú)分出來,估計(jì)是性能考慮;
- 棧: 程序運(yùn)行記錄,每個(gè)線程,也就是每個(gè)執(zhí)行序列各有一個(gè)(看crash log最容易理解),都是編譯的時(shí)候能確定好的,還有一個(gè)特點(diǎn)就是這里面的數(shù)據(jù)可以不用指針,也不會(huì)丟;
- 堆: 最靈活的內(nèi)存區(qū),用途多多,動(dòng)態(tài)分配和釋放,編譯時(shí)不能提前確定,我們的Objective-C對象都是這么來的,都存在這里,通常堆中的對象都是以指針來訪問的,指針從線程棧中來,但不獨(dú)屬于某個(gè)線程,堆也是對復(fù)雜的運(yùn)行時(shí)處理的基礎(chǔ)支持;
MRC 所說的“誰分配誰釋放”說的都是堆上對象的管理;
總結(jié): 通過內(nèi)存分布,不難發(fā)現(xiàn)我們所要管理的基本就是堆 和 棧了!
四. 堆和棧的分析:
堆棧具體特性,百度大把,不做闡述,下面簡述分配與訪問:
-
4.1 堆
分配 : 執(zhí)行時(shí)動(dòng)態(tài)分配和釋放,編譯器不能確定具體值,此特性也奠定了OC 動(dòng)態(tài)語言的基礎(chǔ),內(nèi)存管理的主要區(qū)段;(引用類型存儲(chǔ)區(qū),也就是OC對象)
訪問:內(nèi)存的分配原則(init 采用循環(huán)首次適應(yīng)算法),導(dǎo)致了堆內(nèi)有大量的碎片存在,有效數(shù)據(jù)區(qū)并非連續(xù),通常通過棧區(qū)訪問,由棧去的指針指向堆去的數(shù)據(jù)區(qū),從而找到數(shù)據(jù)對應(yīng)的位置;
-
4.2 棧
分配: 依次緊密排列,遵循先進(jìn)后出(FILO)的原則,也就決定了其對小數(shù)據(jù)塊讀寫相對較快;一般用作存儲(chǔ)值類型數(shù)據(jù)(基本數(shù)據(jù)類型)
訪問: 通過邏輯地址映射為物理地址,由CPU通過總線進(jìn)行數(shù)據(jù)訪問;
上述分析涉及一個(gè)基本類型和OC對象存儲(chǔ)的問題,基本數(shù)據(jù)類型在棧區(qū),OC對象在堆區(qū),那么如若對數(shù)據(jù)進(jìn)行裝箱和拆箱操作,勢必會(huì)帶來額外的內(nèi)存開銷,所以,不要不關(guān)心數(shù)據(jù)類型!
隨帶引出野指針和內(nèi)存泄露的概念:
- 野指針: 對應(yīng)堆區(qū)數(shù)據(jù)已經(jīng)釋放,但是棧區(qū)指針的指針并未被清空;
- 內(nèi)存泄露: 對應(yīng)棧區(qū)的指針已被清空,但其所指向的堆區(qū)內(nèi)存并未釋放;
- 空指針: 沒有對應(yīng)的堆區(qū),即沒有指向的存儲(chǔ)空間;
五. 內(nèi)存相關(guān)的修飾符
- strong :強(qiáng)引用,ARC中使用,與MRC中retain類似,使用之后,計(jì)數(shù)器+1。
- weak :弱引用 ,ARC中使用,如果只想的對象被釋放了,其指向nil,可以有效的避免野指針,其引用計(jì)數(shù)為1。
- readwrite : 可讀可寫特性,需要生成getter方法和setter方法時(shí)使用。
- readonly : 只讀特性,只會(huì)生成getter方法 不會(huì)生成setter方法,不希望屬性在類外改變。
- assign :賦值特性,不涉及引用計(jì)數(shù),弱引用,setter方法將傳入?yún)?shù)賦值給實(shí)例變量,僅設(shè)置變量時(shí)使用。
- retain :表示持有特性,setter方法將傳入?yún)?shù)先保留,再賦值,傳入?yún)?shù)的retaincount會(huì)+1。
- copy :表示拷貝特性,setter方法將傳入對象復(fù)制一份,需要完全一份新的變量時(shí)。
- nonatomic :非原子操作,不加同步,多線程訪問可提高性能,但是線程不安全的。決定編譯器生成的setter getter是否是原子操作。
- atomic :原子操作,同步的,表示多線程安全,與nonatomic相反。(所以,在并發(fā)讀寫的時(shí)候可以考慮直接使用atomic,而不是使用各種鎖/信號量等)
六. 看完內(nèi)存分配算法順便分析一下 alloc 和 new 的區(qū)別:
alloc 和 new 最終都要調(diào)用底層的 malloc 函數(shù)族;
直接上源碼:
- new
+new{
id newObject = (*_alloc)((Class)self, 0);
Class metaClass = self->isa;
if (class_getVersion(metaClass) > 1)
return ;
else
return newObject;
}
- alloc
+alloc
{
return (*_zoneAlloc)((Class)self, 0, malloc_default_zone());
}
很明顯,區(qū)別只在于alloc分配內(nèi)存的時(shí)候使用了zone,這個(gè)zone是個(gè)什么東東呢?它是給對象分配內(nèi)存的時(shí)候,把關(guān)聯(lián)的對象分配到一個(gè)相鄰的 內(nèi)存區(qū)域內(nèi)(循環(huán)首次適應(yīng)算法 ),以便于調(diào)用時(shí)消耗很少的代價(jià),提升了程序處理速度;
new 和 malloc 區(qū)別:
- new 是c++中的操作符,malloc是c 中的一個(gè)函數(shù)
- new 不止是分配內(nèi)存,而且會(huì)調(diào)用類的構(gòu)造函數(shù),同理delete會(huì)調(diào)用類的析構(gòu)函數(shù),而malloc則只分配內(nèi)存,不會(huì)進(jìn)行初始化類成員的工作,同樣free也不會(huì)調(diào)用析構(gòu)函數(shù)
- 內(nèi)存泄漏對于malloc或者new都可以檢查出來的,區(qū)別在于new可以指明是那個(gè)文件的那一行, 而malloc沒有這些信息。
new 和 malloc效率比較
- new可以認(rèn)為是malloc加構(gòu)造函數(shù)的執(zhí)行。
- new出來的指針是直接帶類型信息的。