iOS 內(nèi)存管理(一)-分區(qū)及引用計(jì)數(shù)

一、內(nèi)存管理五大區(qū)

在iOS中內(nèi)存主要分為五大區(qū)域:棧區(qū)、堆區(qū)、靜態(tài)區(qū)、常量區(qū)、代碼段。


1.棧區(qū)

棧區(qū)是一段連續(xù)的內(nèi)存區(qū)域,從高地址向低地址存儲,遵循先進(jìn)后出(FILO)原則。
在 x86 架構(gòu)下,棧的地址一般為 0X7 開頭。
一般在運(yùn)行時進(jìn)行分配,內(nèi)存空間由系統(tǒng)管理,變量過了作用域范圍后內(nèi)存便會自動釋放。
參數(shù)、函數(shù)、局部變量都放在棧區(qū)
參數(shù)入棧是從前往后入棧。而結(jié)構(gòu)體入棧是從后往前入棧。
通過 sp 寄存器直接定位。

2.堆區(qū)

堆區(qū)是不連續(xù)的內(nèi)存從低地址向高地址存儲,遵循先進(jìn)先出(FIFO)原則。
堆的地址空間 iOS x86 架構(gòu)下以 0X6 開頭,空間的分配是動態(tài)的。
需要關(guān)注變量的生命周期,不及時釋放會造成內(nèi)存泄露。
OC 中使用 alloc、new 開辟空間創(chuàng)建的對象內(nèi)存放在堆區(qū)(而指向內(nèi)存的指針還是在棧里)。
C語言中使用 malloccalloc、realloc 分配的空間,需要 free 釋放。
通過 sp 寄存器來定位到棧內(nèi)存地址,通過該地址定位堆內(nèi)存地址,所以說棧定位比堆定位速度快。

3.棧區(qū)與堆區(qū)對比
  • 棧是一段連續(xù)的內(nèi)存區(qū)域,堆是不連續(xù)的內(nèi)存。
  • 棧系統(tǒng)自動回收內(nèi)存,堆需要開發(fā)人員手動釋放。
  • 棧內(nèi)存大小有限制,內(nèi)存空間小,堆內(nèi)存空間大。
4.全局靜態(tài)區(qū)

全局靜態(tài)區(qū)是編譯時分配的內(nèi)存空間,在 iOS 中一般以 0x1 開頭,程序運(yùn)行過程中,此內(nèi)存中的數(shù)據(jù)一直存在,程序結(jié)束后由系統(tǒng)釋放。
未初始化的全局變量和靜態(tài)變量,在 BSS 區(qū),即未初始化區(qū),.bss。
已初始化的全局變量和靜態(tài)變量,在數(shù)據(jù)區(qū),即初始化區(qū),.data

5.常量區(qū)

常量區(qū)是編譯時分配的內(nèi)存空間,在程序運(yùn)行過程中,此內(nèi)存中的數(shù)據(jù)一直存在,程序結(jié)束后由系統(tǒng)釋放。
存放常量:整型、字符型、浮點(diǎn)、字符串等。

6.代碼區(qū)

代碼區(qū)編譯時分配的內(nèi)存空間,在程序運(yùn)行過程中,此內(nèi)存中的數(shù)據(jù)一直存在,程序結(jié)束后由系統(tǒng)釋放。
程序運(yùn)行時的代碼會被編譯成二進(jìn)制,存進(jìn)內(nèi)存的代碼區(qū)域。

二、內(nèi)存管理機(jī)制

隨著各個平臺的發(fā)展,現(xiàn)在被廣泛采用的內(nèi)存管理機(jī)制主要有 GC 和 RC 兩種。
GC (Garbage Collection):垃圾回收機(jī)制,定期查找不再使用的對象,釋放對象占用的內(nèi)存。
RC (Reference Counting):引用計(jì)數(shù)機(jī)制。采用引用計(jì)數(shù)來管理對象的內(nèi)存,當(dāng)需要持有一個對象時,使它的引用計(jì)數(shù) +1;當(dāng)不需要持有一個對象的時候,使它的引用計(jì)數(shù) -1;當(dāng)一個對象的引用計(jì)數(shù)為 0,該對象就會被銷毀。

Objective-C 支持三種內(nèi)存管理機(jī)制:ARC、MRC 和 GC,但 Objective-C 的 GC 機(jī)制有平臺局限性,僅限于 MacOS 開發(fā)中,iOS 開發(fā)用的是 RC 機(jī)制,從 MRC 到現(xiàn)在的 ARC。

引用計(jì)數(shù)的存儲

以上我們對 “引用計(jì)數(shù)” 這一概念做了初步了解,Objective-C 中的 “對象” 通過引用計(jì)數(shù)功能來管理它的內(nèi)存生命周期。那么,對象的引用計(jì)數(shù)是如何存儲的呢?它存儲在哪個數(shù)據(jù)結(jié)構(gòu)里?

首先,不得不提一下 isa。
isa 指針用來維護(hù) “對象” 和 “類” 之間的關(guān)系,并確保對象和類能夠通過 isa 指針找到對應(yīng)的方法、實(shí)例變量、屬性、協(xié)議等;
在 arm64 架構(gòu)之前,isa 就是一個普通的指針,直接指向 objc_class,存儲著Class、Meta-Class 對象的內(nèi)存地址。instance 對象的 isa 指向 class 對象,class 對象的 isa 指向 meta-class 對象;
從 arm64 架構(gòu)開始,對 isa 進(jìn)行了優(yōu)化,用 nonpointer 表示,變成了一個共用體(union)結(jié)構(gòu),還使用位域來存儲更多的信息。將 64 位的內(nèi)存數(shù)據(jù)分開來存儲著很多的東西,其中的 33 位才是拿來存儲 classmeta-class 對象的內(nèi)存地址信息。要通過位運(yùn)算將 isa 的值 & ISA_MASK 掩碼,才能得到 class、meta-class 對象的內(nèi)存地址。

// objc.h
struct objc_object {
    Class isa;  // 在 arm64 架構(gòu)之前
};

// objc-private.h
struct objc_object {
private:
    isa_t isa;  // 在 arm64 架構(gòu)開始
};

union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;

#if SUPPORT_PACKED_ISA

    // extra_rc must be the MSB-most field (so it matches carry/overflow flags)
    // nonpointer must be the LSB (fixme or get rid of it)
    // shiftcls must occupy the same bits that a real class pointer would
    // bits + RC_ONE is equivalent to extra_rc + 1
    // RC_HALF is the high bit of extra_rc (i.e. half of its range)

    // future expansion:
    // uintptr_t fast_rr : 1;     // no r/r overrides
    // uintptr_t lock : 2;        // lock for atomic property, @synch
    // uintptr_t extraBytes : 1;  // allocated with extra bytes

# if __arm64__  // 在 __arm64__ 架構(gòu)下
#   define ISA_MASK        0x0000000ffffffff8ULL  // 用來取出 Class、Meta-Class 對象的內(nèi)存地址
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {
        uintptr_t nonpointer        : 1;  // 0:代表普通的指針,存儲著 Class、Meta-Class 對象的內(nèi)存地址
                                          // 1:代表優(yōu)化過,使用位域存儲更多的信息
        uintptr_t has_assoc         : 1;  // 是否有設(shè)置過關(guān)聯(lián)對象,如果沒有,釋放時會更快
        uintptr_t has_cxx_dtor      : 1;  // 是否有C++的析構(gòu)函數(shù)(.cxx_destruct),如果沒有,釋放時會更快
        uintptr_t shiftcls          : 33; // 存儲著 Class、Meta-Class 對象的內(nèi)存地址信息
        uintptr_t magic             : 6;  // 用于在調(diào)試時分辨對象是否未完成初始化
        uintptr_t weakly_referenced : 1;  // 是否有被弱引用指向過,如果沒有,釋放時會更快
        uintptr_t deallocating      : 1;  // 對象是否正在釋放
        uintptr_t has_sidetable_rc  : 1;  // 如果為1,代表引用計(jì)數(shù)過大無法存儲在 isa 中,那么超出的引用計(jì)數(shù)會存儲在一個叫 SideTable 結(jié)構(gòu)體的 RefCountMap(引用計(jì)數(shù)表)散列表中
        uintptr_t extra_rc          : 19; // 里面存儲的值是對象本身之外的引用計(jì)數(shù)的數(shù)量,retainCount - 1
#       define RC_ONE   (1ULL<<45)
#       define RC_HALF  (1ULL<<18)
    };
......  // 在 __x86_64__ 架構(gòu)下
};

如果 isanonpointer,即 arm64 架構(gòu)之前的 isa 指針。由于它只是一個普通的指針,所以它本身不能存儲引用計(jì)數(shù),所以以前對象的引用計(jì)數(shù)都存儲在一個叫 SideTable 結(jié)構(gòu)體的 RefCountMap(引用計(jì)數(shù)表)散列表中。

如果 isanonpointer,則它本身可以存儲一些引用計(jì)數(shù)。從以上 union isa_t 的定義中我們可以得知,isa_t 中存儲了兩個引用計(jì)數(shù)相關(guān)的東西:extra_rchas_sidetable_rc。
extra_rc:里面存儲的值是對象本身之外的引用計(jì)數(shù)的數(shù)量,這 19 位如果不夠存儲,has_sidetable_rc 的值就會變?yōu)?1;
has_sidetable_rc:如果為 1,代表引用計(jì)數(shù)過大無法存儲在 isa 中,那么超出的引用計(jì)數(shù)會存儲 SideTableRefCountMap 中。
所以,如果 isanonpointer,則對象的引用計(jì)數(shù)存儲在它的 isa_textra_rc 中以及 SideTableRefCountMap 中。

SideTable

以上提到了一個數(shù)據(jù)結(jié)構(gòu) SideTable,我們進(jìn)入 objc4 源碼查看它的定義。

// NSObject.mm
struct SideTable {
    spinlock_t slock;        // 自旋鎖
    RefcountMap refcnts;     // 引用計(jì)數(shù)表(散列表)
    weak_table_t weak_table; // 弱引用表(散列表)
    ......
}

SideTable 存儲在 SideTables() 中,SideTables() 本質(zhì)也是一個散列表,可以通過對象指針來獲取它對應(yīng)的(引用計(jì)數(shù)表或者弱引用表)在哪一個 SideTable中。在非嵌入式系統(tǒng)下,SideTables() 中有 64 個 SideTable。以下是SideTables() 的定義:

// NSObject.mm
static objc::ExplicitInit<StripedMap<SideTable>> SideTablesMap;

static StripedMap<SideTable>& SideTables() {
    return SideTablesMap.get();
}

所以,查找對象的引用計(jì)數(shù)表需要經(jīng)過兩次哈希查找:
(1)第一次根據(jù)當(dāng)前對象的內(nèi)存地址,經(jīng)過哈希查找從 SideTables()中取出它所在的 SideTable;
(2)第二次根據(jù)當(dāng)前對象的內(nèi)存地址,經(jīng)過哈希查找從 SideTable 中的 refcnts 中取出它的引用計(jì)數(shù)表。
為什么不是一個 SideTable,而是使用多個 SideTable 組成 SideTables() 結(jié)構(gòu)?
如果只有一個 SideTable,那我們在內(nèi)存中分配的所有對象的引用計(jì)數(shù)或者弱引用都放在這個 SideTable 中,那我們對對象的引用計(jì)數(shù)進(jìn)行操作時,為了多線程安全就要加鎖,就存在效率問題。
系統(tǒng)為了解決這個問題,就引入 “分離鎖” 技術(shù)方案,提高訪問效率。把對象的引用計(jì)數(shù)表分拆多個部分,對每個部分分別加鎖,那么當(dāng)所屬不同部分的對象進(jìn)行引用操作的時候,在多線程下就可以并發(fā)操作。所以,使用多個 SideTable 組成SideTables() 結(jié)構(gòu)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容