内存布局
内存区域分为内核区的内存,程序加载的控件,保留的内存空间
地址的表示是由下到上是低地址到高地址
程序加载到内存会分成三段:未初始化区,已初始化区和代码段
代码段: 我们写的程序所有的代码数据段都在代码段中
已初始化区: 我们声明的已初始化的静态变量,枚举变量
未初始化区: 我们声明的未初始化的静态变量和枚举变量
栈区: 定义的方法和函数都是在栈上工作,栈是从高地址到低地址进行扩展,所以说栈是向下扩展
堆区: 创建的对象,或者block经过copy之后,都会被转移到堆上面,堆是向上增长的
不同内存段作用
stack: 方法调用
heap: 通过alloc等分配的对象
bss: 未初始化的全局变量和静态变量等
data: 已初始化的全局变量等
text: 程序代码,加载到内存后都放在text段中
内存管理方案
iOS不止是用引用计数表和弱引用表来对内存进行管理的
它是针对不同场景,会提供不同的内存管理方案,有以下几种方案
1. TaggedPointer
对一些小对象,如NSNumber等,采用的是TaggedPointer这种内存管理方案
2.NONPOINTER_ISA
对于64位架构下的iOS应用程序采用的是NONPOINTER_ISA这种内存管理方案
在64位架构下,ISA这个指针本身是占64个bit位的,但其实有32位或者40位就够用了,剩余的bit位其实是浪费的,苹果为了提高内存的利用率,在iSA剩余的这些bit位当中,存储了一些关于内存管理方面的相关内容,这个叫非指针型的ISA
3.散列表
是一种很复杂的结构,其中包含了引用计数表和弱引用表
基于objc-runtime-680版本的源码分析,可以在苹果官网获取
NONPOINTER_ISA (非指针型的ISA)
在arm64架构下,ISA指针一共有64个bit位
下面看看这64个bit位分别都存储了什么内容
第一位是indexed标志位,如果这个位置是0,代表的这个ISA指针只是一个ISA指针,里面内容代表当前对象的类对象的地址
若这个标志是1,代表ISA指针里面不仅存储类对象的地址,还有一些内存管理方面的数据
第二位表示当前对象是否有关联对象,若是0则没有,若是1代表有
第三位表示当前对象是否有使用到C++相关的代码
剩下的33位表示当前对象的类对象的指针地址
再后6位magic,没研究
再后一位标识了这个对象是否有相应的一个弱引用指针
再后一位标识当前对象是否正在进行dealloc操作
再后一位标识当前ISA指针中存储的引用计数是否达到了上线,若达到了,需要外挂一个sidetable这样的数据结构来存储相关的引用计数内容
剩余的extra_rc代表的就是额外的引用计数,当引用计数值很小的时候,会存在ISA指针中,当大的时候,会有单独的引用计数表去存储
由上面可以看出,关于内存管理不仅仅是散列表,其实还有ISA部分的extra_rc来存储相关的引用计数值
散列表
散列表的方案在源码中是通过Side Tables()结构来实现
Side Table()结构下挂了很多Side Table这样的数据结构,这些结构在不同的架构下有不同的个数
例如在非嵌入式系统下面,一共有64个Side Table表
Side Table()实际上是一个哈希表
可以通过一个对象指针来具体找到对应的引用计数标或弱引用表在哪一张Side Table中
Side Table结构下包含了三个元素
1.自旋锁
2.引用计数表
3.弱引用表
为什么不是一个Side Table来实现,而是由多个Side Table共同组成一个Side Tables()这样一个数据结构?
假如只有一张Side Table,相当于我们在内存当中分配的所有对象的引用计数都放到了一张大表中,如果要操作某个对象的引用计数值进行修改,由于所有的对象可能是在不同的线程中分配创建的,那么对这张表操作时就需要进行加锁处理,来保证数据访问的安全,这样就存在了效率问题
假如用户的内存空间一共有4GB,我们可能分配出成千上百万个内存对象,如果每一个对象在进行引用计数改变时,都操作这张表,当对象A操作时,因为加了锁,下一个对象就要等当前对象操作完之后,将锁释放后,B才能操作
系统为了解决这样的效率问题,引用了分离锁的技术方案
分离锁:可以把内存对象所对应的引用计数表分拆成多个部分,假设分拆成8个,需要对8个表分别加锁,假如对象A在表1中,对象B在表2中,当A和B同时进行引用计数操作时,可以并发操作,但如果只有一张表就只能按顺序操作,分离锁可以提高访问效率
如何实现快速分流
快速分流是指: 如何通过一个对象指针,如何快速定位到它属于哪张Side Table表
Side Table本质是张Hash表,这张Hash表中,可能有64张具体的Side Table,存储不同对象的引用计数表和弱引用表
Hash表的概念是
对象指针可以作为一个key
经过Hash函数的一个运算,会计算出一个值Value,来决定出这个对象所对应的Side Table是哪张,或者说在数组的位置索引是哪个
下面看下Hash查找的过程
假如给定的值是对象内存地址,目标值是Side Table结构(数组)下标索引
ptr是对象内存指针地址,通过哈希函数f,把指针作为函数f的参数,经过函数f的运算,可以得出数组的下标索引值index
函数f对于Side Table具体的情况来讲,实际表达式如图所示,通过对象的内存地址,来和Side Table这个数组的个数进行取余运算,计算出对象指针所对应的引用技术表或者弱引用表是在哪一张Side Table中
为什么要通过Hash查找
可以提高查找效率,存储时通过Hash进行存储,假如数组是8,假如内存地址是1,取余就是1,我们就把对象存储在第一个位置,当访问对象时,也不用对数组遍历并比较指针值,只需要也通过这个函数进行运算,找到第一个索引位置,直接取出内容
这样就不涉及遍历操作了,查找效率比较高
内存地址的分布是均匀分布,我们可以称这个hash函数为均匀散列函数
散列表
1.自旋锁Spinlock_t
是一种"忙等"的锁,如果当前锁已被其他线程获取,当前线程会不断探测这个锁有没有被释放,如果被释放了,线程就会第一时间去获取这个锁
其他锁,比如信号量:当它获取不到这个锁时,会把自己的线程进行阻塞休眠,然后等到其他线程释放这个锁的时候,再唤醒当前线程
自旋锁适用于轻量访问
2.引用计数表RefcountMap
引用计数表是哈希表,可以理解为是一个字典,可以通过指针,找到对应对象的引用计数,这个查找过程是一个哈希查找,这个哈希算法实际上是对传入对象的指针做一个伪装的操作,然后去获取对应的引用计数
之所以使用哈希查找,是为了提高查找效率,之所以能提高查找效率,是因为我们存储一个对象的引用计数时,是通过同一个函数来计算存储位置的,而获取对象的引用计数值的时候,也通过同一个函数来计算应该获取的索引位置
因为插入和获取都是通过同一个函数来计算位置,就会避免循环遍历的操作,所以才说,哈希查找可以提高查找效率
size_t表达的就是对象的引用计数值,是一个无符号long型的变量
假如引用计数存储是用64位来表示的
第1个二进制位表示对象是否有弱引用
第2位表示当前对象是否正在delloc
其他存储这个对象的实际引用计数值
当我们计算对象的引用计数时,需要对这个值进行向右偏移两位,因为要去掉后面两位,才可以取到真实的引用计数值
3.弱引用表weak_table_t
在源码中可以看到,弱引用表示根据weak_table_t来定义的,也是一张哈希表,给与一个对象的指针作为key,通过一个哈希函数,就可以计算出对应的弱引用的对象的存储位置
weak_entry_t实际上也是一个结构体数组,这个数组中存储的每一个对象就是实际的弱引用指针,也就是我们在代码当中定义的类似于__weak id obj,那么这个obj内存地址就存储在weak_entry_t这个结构体数组中