iOS 内存管理(一)-分区及引用计数
一、内存管理五大区
在iOS中内存主要分为五大区域:栈区、堆区、静态区、常量区、代码段。
1.栈区
栈区是一段连续的内存区域,从高地址向低地址存储,遵循先进后出(FILO)原则。
在 x86 架构下,栈的地址一般为 0X7 开头。
一般在运行时进行分配,内存空间由系统管理,变量过了作用域范围后内存便会自动释放。
参数、函数、局部变量都放在栈区
参数入栈是从前往后入栈。而结构体入栈是从后往前入栈。
通过 sp
寄存器直接定位。
2.堆区
堆区是不连续的内存从低地址向高地址存储,遵循先进先出(FIFO)原则。
堆的地址空间 iOS x86 架构下以 0X6 开头,空间的分配是动态的。
需要关注变量的生命周期,不及时释放会造成内存泄露。
OC 中使用 alloc
、new
开辟空间创建的对象内存放在堆区(而指向内存的指针还是在栈里)。
C语言中使用 malloc
、calloc
、realloc
分配的空间,需要 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
,存储着Class
、Meta-Class
对象的内存地址。instance
对象的 isa
指向 class
对象,class
对象的 isa
指向 meta-class
对象;
从 arm64 架构开始,对 isa
进行了优化,用 nonpointer
表示,变成了一个共用体(union)结构,还使用位域来存储更多的信息。将 64 位的内存数据分开来存储着很多的东西,其中的 33 位才是拿来存储 class
、meta-class
对象的内存地址信息。要通过位运算将 isa
的值 & ISA_MASK
掩码,才能得到 class
、meta-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__ 架构下
};
如果 isa
非 nonpointer
,即 arm64 架构之前的 isa
指针。由于它只是一个普通的指针,所以它本身不能存储引用计数,所以以前对象的引用计数都存储在一个叫 SideTable
结构体的 RefCountMap
(引用计数表)散列表中。
如果 isa
是 nonpointer
,则它本身可以存储一些引用计数。从以上 union isa_t
的定义中我们可以得知,isa_t
中存储了两个引用计数相关的东西:extra_rc
和has_sidetable_rc
。
extra_rc
:里面存储的值是对象本身之外的引用计数的数量,这 19 位如果不够存储,has_sidetable_rc
的值就会变为 1;
has_sidetable_rc
:如果为 1,代表引用计数过大无法存储在 isa
中,那么超出的引用计数会存储 SideTable
的 RefCountMap
中。
所以,如果 isa
是 nonpointer
,则对象的引用计数存储在它的 isa_t
的 extra_rc
中以及 SideTable
的 RefCountMap
中。
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()
结构。