內(nèi)存布局
上面的圖代表一個內(nèi)存區(qū)域震放,內(nèi)存區(qū)域分為內(nèi)核區(qū)的內(nèi)存(最上邊),程序加載的控件(中間),保留的內(nèi)存空間(最下面)。
地址的表示是由下到上是低地址到高地址漠趁。
比如說程序加載到內(nèi)存會分成三段:未初始化區(qū),已初始化區(qū)和代碼段:
- 代碼段: 我們寫的程序所有的代碼數(shù)據(jù)段都在代碼段(.text)中熏兄。
- 已初始化區(qū): 我們聲明的已初始化的靜態(tài)變量,全局變量都在已初始化數(shù)據(jù)區(qū)(.data)中躺屁。
- 未初始化區(qū): 我們聲明的未初始化的靜態(tài)變量和枚舉變量都在未初始化數(shù)據(jù)區(qū)(.bss)中。
- 棧區(qū)(stack): iOS定義的方法和函數(shù)都是在棧上工作,棧是從高地址到低地址進(jìn)行擴(kuò)展,所以說棧是向下擴(kuò)展蹈集。
- 堆區(qū)(heap): 創(chuàng)建的對象,或者block經(jīng)過copy之后,都會被轉(zhuǎn)移到堆上面,堆是向上增長的烁试。
不同內(nèi)存段分別代表的詳細(xì)含義:
stack: 代表棧區(qū),棧區(qū)一般都是方法調(diào)用會在這個內(nèi)存區(qū)進(jìn)行展開拢肆。
heap: 代表堆區(qū)减响,通過alloc等分配的對象,實際上都是在堆上面體現(xiàn)的郭怪。
bss: 未初始化的全局變量/靜態(tài)變量等支示。
data: 已初始化的全局變量等。
text: 程序代碼,加載到內(nèi)存后都放在text段中鄙才。
內(nèi)存管理方案
iOS操作系統(tǒng)是怎么對內(nèi)存進(jìn)行管理的颂鸿?
iOS操作系統(tǒng)是針對不同場景,會提供不同的內(nèi)存管理方案,有以下幾種方案:
1.TaggedPointer
對一些小對象,如NSNumber等,采用的是TaggedPointer這種內(nèi)存管理方案。
2.NONPOINTER_ISA
對于64位架構(gòu)下的iOS應(yīng)用程序采用的是NONPOINTER_ISA這種內(nèi)存管理方案攒庵。
在64位架構(gòu)下,ISA這個指針本身是占64個bit位的,但其實有32位或者40位就夠用了,剩余的bit位其實是浪費(fèi)的,蘋果為了提高內(nèi)存的利用率,在iSA剩余的這些bit位當(dāng)中,存儲了一些關(guān)于內(nèi)存管理方面的相關(guān)內(nèi)容,這個叫非指針型的ISA嘴纺。
3.散列表
是一種很復(fù)雜的結(jié)構(gòu),其中包含了引用計數(shù)表和弱引用表败晴。
NONPOINTER_ISA (非指針型的ISA)這種內(nèi)存管理方案)
在arm64架構(gòu)下,ISA指針一共有64個bit位,我們逐一分析這64個bit位分別都存儲了什么內(nèi)容:
首先看0-15位:第一位是(indexed)標(biāo)志位,如果這個位置是0,代表我們使用的這個ISA指針只是一個純的ISA指針,里面內(nèi)容代表當(dāng)前對象的類對象的地址栽渴。若這個標(biāo)志是1,代表ISA指針里面不僅存儲類對象的地址,還有一些內(nèi)存管理方面的數(shù)據(jù)尖坤。
第二位(has_assoc)表示當(dāng)前對象是否有關(guān)聯(lián)對象,若是0則沒有,若是1代表有。
第三位(has_cxx_dtor)表示當(dāng)前對象是否有使用到C++相關(guān)的代碼
剩下的33位(shiftcls)0,1的一個bit位表示當(dāng)前對象的類對象的指針地址闲擦。
再后6位(magic),不影響內(nèi)存管理的解答慢味。
再后一位(weakly_referenced)標(biāo)識了這個對象是否有相應(yīng)的一個弱引用指針。
再后一位(deallocating)標(biāo)識當(dāng)前對象是否正在進(jìn)行dealloc操作佛致。
再后一位(has_siderable_rc)標(biāo)識當(dāng)前ISA指針中存儲的引用計數(shù)是否達(dá)到了上線,若達(dá)到了,需要外掛一個sidetable這樣的數(shù)據(jù)結(jié)構(gòu)來存儲相關(guān)的引用計數(shù)內(nèi)容,也就是我們接下來要了解的散列表贮缕。
剩余的(extra_rc)代表的就是額外的引用計數(shù),當(dāng)引用計數(shù)值很小的時候,會存在ISA指針中,當(dāng)大的時候,會有單獨(dú)的引用計數(shù)表去存儲。
通過NONPOINTER_ISA 64個bit位的分析俺榆,可以看出,關(guān)于內(nèi)存管理不僅僅是散列表,其實還有ISA部分的extra_rc來存儲相關(guān)的引用計數(shù)值感昼。
散列表方式(關(guān)于散列表這種內(nèi)存管理方案的相關(guān)面試問題)
散列表的方案在源碼中是通過Side Tables()結(jié)構(gòu)來實現(xiàn),Side Tables()結(jié)構(gòu)是什么:
Side Table()結(jié)構(gòu)下掛了很多Side Table這樣的數(shù)據(jù)結(jié)構(gòu),這些結(jié)構(gòu)在不同的架構(gòu)下有不同的個數(shù)罐脊。
例如在非嵌入式系統(tǒng)下面,一共有64個Side Table表定嗓。
Side Table()實際上是一個哈希表
可以通過一個對象指針來具體找到對應(yīng)的引用計數(shù)標(biāo)或弱引用表在哪一張Side Table中。
Side Table結(jié)構(gòu)下包含了三個元素
1.自旋鎖
2.引用計數(shù)表
3.弱引用表
面試當(dāng)中進(jìn)程會針對引用計數(shù)表和弱引用表提出一些相關(guān)技術(shù)問題萍桌。
也會有一些涉及到自旋鎖的相關(guān)面試問題宵溅,不過涉及到一些多線程的和資源競爭方面相關(guān)的問題。
為什么不是一個Side Table來實現(xiàn),而是由多個Side Table共同組成一個Side Tables()這樣一個數(shù)據(jù)結(jié)構(gòu)?
假如只有一張Side Table,相當(dāng)于我們在內(nèi)存當(dāng)中分配的所有對象的引用計數(shù)或者說弱引用都放到了一張大表中,
如果要操作某個對象的引用計數(shù)值進(jìn)行修改(進(jìn)行+1或者-1的操作),
由于所有的對象可能是在不同的線程中分配創(chuàng)建的(包括調(diào)用他們的return或者release等方法上炎,也可能是在不同線程里面進(jìn)行操作的),
那么對這張表操作時就需要進(jìn)行加鎖處理
,來保證數(shù)據(jù)訪問的安全,這樣就存在了效率問題
恃逻。
假如用戶的內(nèi)存空間一共有4GB,我們可能分配出成千上百萬個內(nèi)存對象,
如果每一個對象在進(jìn)行引用計數(shù)改變時,都操作這張表,很顯然就存在了效率問題。
當(dāng)對象A操作時,因為加了鎖,下一個對象就要等當(dāng)前對象操作完之后,將鎖釋放后,B才能操作藕施。
系統(tǒng)為了解決這樣的效率問題,引用了分離鎖
的技術(shù)方案寇损。
分離鎖:可以把內(nèi)存對象所對應(yīng)的引用計數(shù)表分拆成多個部分,假設(shè)分拆成8個,需要對8個表分別加鎖,
假如對象A在表1中,對象B在表2中,當(dāng)A和B同時進(jìn)行引用計數(shù)操作時,可以并發(fā)操作,
但如果只有一張表就只能按順序操作,分離鎖可以提高訪問效率.
如何實現(xiàn)快速分流(如何通過一個對象指針,如何快速定位到它屬于哪張Side Table表)?
快速分流是指: Side Table本質(zhì)是張Hash表,這張Hash表中,可能有64張具體的Side Table,
存儲不同對象的引用計數(shù)表和弱引用表
Hash表的概念是:(哈希查找 裳食、哈希算法)
對象指針可以作為一個key
經(jīng)過Hash函數(shù)的一個運(yùn)算,會計算出一個值Value,來決定出這個對象所對應(yīng)的Side Table是哪張,或者說在數(shù)組的位置索引是哪個矛市。
下面看下Hash查找的過程
假如給定的值是對象內(nèi)存地址,目標(biāo)值是Side Table結(jié)構(gòu)(數(shù)組)下標(biāo)索引
ptr是對象內(nèi)存指針地址。
通過哈希函數(shù)f,把指針ptr作為函數(shù)f的參數(shù)诲祸。
經(jīng)過函數(shù)f的運(yùn)算,可以得出數(shù)組的下標(biāo)索引值index浊吏。
哈希函數(shù)f對于Side Table具體的情況來講,實際表達(dá)式如圖所示f(ptr) = (unitptr_t)ptr % array.count,通過對象的內(nèi)存地址,來和Side Table這個數(shù)組的個數(shù)進(jìn)行取余運(yùn)算。
計算出對象指針?biāo)鶎?yīng)的引用技術(shù)表或者弱引用表是在哪一張Side Table中救氯。
為什么要通過Hash查找找田?
- 可以提高查找效率。
存儲時通過Hash進(jìn)行存儲,假如數(shù)組是8,假如內(nèi)存地址是1,取余就是1,我們就把對象存儲在第一個位置,
當(dāng)訪問對象時,也不用對數(shù)組遍歷并比較指針值,
只需要也通過這個函數(shù)進(jìn)行運(yùn)算,找到第一個索引位置,直接取出內(nèi)容径密。 - 這樣就不涉及遍歷操作了,查找效率比較高午阵。
- 內(nèi)存地址的分布是均勻分布,我們可以稱這個hash函數(shù)為均勻散列函數(shù)。
散列表數(shù)據(jù)結(jié)構(gòu)(有關(guān)散列表實現(xiàn)的內(nèi)存管理方案涉及到的一些數(shù)據(jù)結(jié)構(gòu))
1.自旋鎖Spinlock_t(你是否使用過自旋鎖,自旋鎖和普通鎖有什么區(qū)別底桂,自旋鎖有哪些使用場景呢植袍?)
- 是一種"忙等"的鎖,如果當(dāng)前鎖已被其他線程獲取,當(dāng)前線程會不斷探測這個鎖有沒有被釋放,
如果被釋放了,線程就會第一時間去獲取這個鎖。 - 比如說其他的鎖,比如信號量:當(dāng)它獲取不到這個鎖時,會把自己的線程進(jìn)行阻塞休眠,
然后等到其他線程釋放這個鎖的時候,再喚醒當(dāng)前線程籽懦。 - 自旋鎖適用于輕量訪問于个。(比如說上面Side Table表,如果說我們對某一個對象來進(jìn)行引用計數(shù)操作的話暮顺,來訪問這個表厅篓,
實際上做+1-1操作是非常快的操作捶码,那么我們可以把它定義為輕量訪問羽氮。我們在這種輕量訪問的場景下,可以使用自旋鎖惫恼。)
2.引用計數(shù)表RefcountMap
1.引用計數(shù)表是哈希表,可以理解為是一個字典,
可以通過指針,找到對應(yīng)對象的引用計數(shù),這個查找過程是一個哈希查找档押。
2.這個哈希算法實際上是對傳入對象的指針做一個偽裝的操作,然后去獲取對應(yīng)的引用計數(shù)(size_t)。
3.之所以使用哈希查找,是為了提高查找效率祈纯。
4.size_t表達(dá)的就是對象的引用計數(shù)值,是一個無符號long型的變量令宿。
- 查找效率的提高,是因為我們存儲一個對象的引用計數(shù)時,是通過同一個函數(shù)來計算存儲位置的,
而獲取對象的引用計數(shù)值的時候,也通過同一個函數(shù)來計算應(yīng)該獲取的索引位置
因為插入和獲取都是通過同一個函數(shù)來計算位置,就會避免循環(huán)遍歷的操作。 - 所以才說,哈希查找可以提高查找效率腕窥。
size_t每一個bit為代表的含義:
假如引用計數(shù)存儲是用64位來表示的
- 第1個二進(jìn)制位(weakly_referenced)表示對象是否有弱引用粒没。
- 第2位(deallocating)表示當(dāng)前對象是否正在delloc。
- 其他(RC)存儲這個對象的實際引用計數(shù)值簇爆。
- 當(dāng)我們計算對象的引用計數(shù)時,需要對這個值進(jìn)行向右偏移兩位,因為要去掉后面兩位,才可以取到真實的引用計數(shù)值癞松。
3.弱引用表weak_table_t
- 在Runtime源碼中可以看到,弱引用表示根據(jù)weak_table_t來定義的,
weak_table_t也是一張哈希表,給與一個對象的指針作為key,
通過一個哈希函數(shù),就可以計算出對應(yīng)的弱引用的對象的存儲位置。 - weak_entry_t實際上也是一個結(jié)構(gòu)體數(shù)組,
這個數(shù)組中存儲的每一個對象就是實際的弱引用指針,
也就是我們在代碼當(dāng)中定義的類似于__weak id obj,
那么這個obj內(nèi)存地址或者說這個指針就存儲在weak_entry_t這個結(jié)構(gòu)體數(shù)組中入蛆。