iOS 内存管理(一)-分区及引用计数

一、内存管理五大区

在iOS中内存主要分为五大区域:栈区、堆区、静态区、常量区、代码段。


1.栈区

栈区是一段连续的内存区域,从高地址向低地址存储,遵循先进后出(FILO)原则。
在 x86 架构下,栈的地址一般为 0X7 开头。
一般在运行时进行分配,内存空间由系统管理,变量过了作用域范围后内存便会自动释放。
参数、函数、局部变量都放在栈区
参数入栈是从前往后入栈。而结构体入栈是从后往前入栈。
通过 sp 寄存器直接定位。

2.堆区

堆区是不连续的内存从低地址向高地址存储,遵循先进先出(FIFO)原则。
堆的地址空间 iOS x86 架构下以 0X6 开头,空间的分配是动态的。
需要关注变量的生命周期,不及时释放会造成内存泄露。
OC 中使用 allocnew 开辟空间创建的对象内存放在堆区(而指向内存的指针还是在栈里)。
C语言中使用 malloccallocrealloc 分配的空间,需要 free 释放。
通过 sp 寄存器来定位到栈内存地址,通过该地址定位堆内存地址,所以说栈定位比堆定位速度快。

3.栈区与堆区对比
  • 栈是一段连续的内存区域,堆是不连续的内存。
  • 栈系统自动回收内存,堆需要开发人员手动释放。
  • 栈内存大小有限制,内存空间小,堆内存空间大。
4.全局静态区

全局静态区是编译时分配的内存空间,在 iOS 中一般以 0x1 开头,程序运行过程中,此内存中的数据一直存在,程序结束后由系统释放。
未初始化的全局变量和静态变量,在 BSS 区,即未初始化区,.bss
已初始化的全局变量和静态变量,在数据区,即初始化区,.data

5.常量区

常量区是编译时分配的内存空间,在程序运行过程中,此内存中的数据一直存在,程序结束后由系统释放。
存放常量:整型、字符型、浮点、字符串等。

6.代码区

代码区编译时分配的内存空间,在程序运行过程中,此内存中的数据一直存在,程序结束后由系统释放。
程序运行时的代码会被编译成二进制,存进内存的代码区域。

二、内存管理机制

随着各个平台的发展,现在被广泛采用的内存管理机制主要有 GC 和 RC 两种。
GC (Garbage Collection):垃圾回收机制,定期查找不再使用的对象,释放对象占用的内存。
RC (Reference Counting):引用计数机制。采用引用计数来管理对象的内存,当需要持有一个对象时,使它的引用计数 +1;当不需要持有一个对象的时候,使它的引用计数 -1;当一个对象的引用计数为 0,该对象就会被销毁。

Objective-C 支持三种内存管理机制:ARC、MRC 和 GC,但 Objective-C 的 GC 机制有平台局限性,仅限于 MacOS 开发中,iOS 开发用的是 RC 机制,从 MRC 到现在的 ARC。

引用计数的存储

以上我们对 “引用计数” 这一概念做了初步了解,Objective-C 中的 “对象” 通过引用计数功能来管理它的内存生命周期。那么,对象的引用计数是如何存储的呢?它存储在哪个数据结构里?

首先,不得不提一下 isa
isa 指针用来维护 “对象” 和 “类” 之间的关系,并确保对象和类能够通过 isa 指针找到对应的方法、实例变量、属性、协议等;
在 arm64 架构之前,isa 就是一个普通的指针,直接指向 objc_class,存储着ClassMeta-Class 对象的内存地址。instance 对象的 isa 指向 class 对象,class 对象的 isa 指向 meta-class 对象;
从 arm64 架构开始,对 isa 进行了优化,用 nonpointer 表示,变成了一个共用体(union)结构,还使用位域来存储更多的信息。将 64 位的内存数据分开来存储着很多的东西,其中的 33 位才是拿来存储 classmeta-class 对象的内存地址信息。要通过位运算将 isa 的值 & ISA_MASK 掩码,才能得到 classmeta-class 对象的内存地址。

// objc.h
struct objc_object {
    Class isa;  // 在 arm64 架构之前
};

// objc-private.h
struct objc_object {
private:
    isa_t isa;  // 在 arm64 架构开始
};

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__ 架构下
#   define ISA_MASK        0x0000000ffffffff8ULL  // 用来取出 Class、Meta-Class 对象的内存地址
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {
        uintptr_t nonpointer        : 1;  // 0:代表普通的指针,存储着 Class、Meta-Class 对象的内存地址
                                          // 1:代表优化过,使用位域存储更多的信息
        uintptr_t has_assoc         : 1;  // 是否有设置过关联对象,如果没有,释放时会更快
        uintptr_t has_cxx_dtor      : 1;  // 是否有C++的析构函数(.cxx_destruct),如果没有,释放时会更快
        uintptr_t shiftcls          : 33; // 存储着 Class、Meta-Class 对象的内存地址信息
        uintptr_t magic             : 6;  // 用于在调试时分辨对象是否未完成初始化
        uintptr_t weakly_referenced : 1;  // 是否有被弱引用指向过,如果没有,释放时会更快
        uintptr_t deallocating      : 1;  // 对象是否正在释放
        uintptr_t has_sidetable_rc  : 1;  // 如果为1,代表引用计数过大无法存储在 isa 中,那么超出的引用计数会存储在一个叫 SideTable 结构体的 RefCountMap(引用计数表)散列表中
        uintptr_t extra_rc          : 19; // 里面存储的值是对象本身之外的引用计数的数量,retainCount - 1
#       define RC_ONE   (1ULL<<45)
#       define RC_HALF  (1ULL<<18)
    };
......  // 在 __x86_64__ 架构下
};

如果 isanonpointer,即 arm64 架构之前的 isa 指针。由于它只是一个普通的指针,所以它本身不能存储引用计数,所以以前对象的引用计数都存储在一个叫 SideTable 结构体的 RefCountMap(引用计数表)散列表中。

如果 isanonpointer,则它本身可以存储一些引用计数。从以上 union isa_t 的定义中我们可以得知,isa_t 中存储了两个引用计数相关的东西:extra_rchas_sidetable_rc
extra_rc:里面存储的值是对象本身之外的引用计数的数量,这 19 位如果不够存储,has_sidetable_rc 的值就会变为 1;
has_sidetable_rc:如果为 1,代表引用计数过大无法存储在 isa 中,那么超出的引用计数会存储 SideTableRefCountMap 中。
所以,如果 isanonpointer,则对象的引用计数存储在它的 isa_textra_rc 中以及 SideTableRefCountMap 中。

SideTable

以上提到了一个数据结构 SideTable,我们进入 objc4 源码查看它的定义。

// NSObject.mm
struct SideTable {
    spinlock_t slock;        // 自旋锁
    RefcountMap refcnts;     // 引用计数表(散列表)
    weak_table_t weak_table; // 弱引用表(散列表)
    ......
}

SideTable 存储在 SideTables() 中,SideTables() 本质也是一个散列表,可以通过对象指针来获取它对应的(引用计数表或者弱引用表)在哪一个 SideTable中。在非嵌入式系统下,SideTables() 中有 64 个 SideTable。以下是SideTables() 的定义:

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

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

所以,查找对象的引用计数表需要经过两次哈希查找:
(1)第一次根据当前对象的内存地址,经过哈希查找从 SideTables()中取出它所在的 SideTable
(2)第二次根据当前对象的内存地址,经过哈希查找从 SideTable 中的 refcnts 中取出它的引用计数表。
为什么不是一个 SideTable,而是使用多个 SideTable 组成 SideTables() 结构?
如果只有一个 SideTable,那我们在内存中分配的所有对象的引用计数或者弱引用都放在这个 SideTable 中,那我们对对象的引用计数进行操作时,为了多线程安全就要加锁,就存在效率问题。
系统为了解决这个问题,就引入 “分离锁” 技术方案,提高访问效率。把对象的引用计数表分拆多个部分,对每个部分分别加锁,那么当所属不同部分的对象进行引用操作的时候,在多线程下就可以并发操作。所以,使用多个 SideTable 组成SideTables() 结构。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,743评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,296评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,285评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,485评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,581评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,821评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,960评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,719评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,186评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,516评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,650评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,936评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,757评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,991评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,370评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,527评论 2 349

推荐阅读更多精彩内容