内存管理(一)
文章目录
- 内存管理(一)
- 内存五大区
- 栈区
- 函数栈
- 堆区
- 全局区(.bss & .data)
- 常量区
- 代码区
- 内存管理
- 内存优化方案
- Tagged Pointer
- NONPOINTER_ISA
- SideTable
- MRC和ARC
- MRC
- ARC
内存五大区
栈区
栈区(stack):编译器自动分配,由系统管理,在不需要的时候自动清除。局部变量,函数的参数储存在这里。栈区的内存地址一般是0x7开头,从高地址到低地址分配内存空间
在iOS中,栈是每个线程独立的,如果创建了一个子线程,子线程也有它自己的栈。因此线程之间不能简单的通过局部变量来直接共享数据
voidA{[selfB];}voidB{[selfC];}voidC{}执行过程是C->回调B->回调C,这就是栈,后进先出,不断递归
iOS主线程大小为1MB 其他线程为512KB MAC为8MB
通过上面的数据我们发现栈区的空间其实比较小,因此递归层级过深或者过多的局部变量,因此我们需要防止栈区内存溢出
函数栈
函数栈在内存中从高地址网低地址分配,与堆区相对
栈帧是指函数(运行中未完成)占用的一块独立的连续内存区域
函数共享线程的这个栈空间,每个函数使用的栈空间是一个栈帧,所有的栈帧组成这个线程完整的栈
函数调用发生在栈上,每个函数的相关信息(局部变量,函数参数,调用记录等)都储存在这个栈帧中,每执行一次函数调用,就会生成一个与其相关的栈帧,然后栈帧压入函数栈,当函数执行结束,就将函数对应的栈帧出栈释放掉
Caller:调用者(图中的main函数),callee:被调用者(图中的func1函数)
如图中所示在执行func1前,目前只有main栈帧,此时可以简单理解为:
高地址-> main栈帧【argc argv i = 10 j = 5】 -> 低地址然后main调用func1,此时CPU从main跳到func1,但并不能简单的跳转,因为func1执行完后还要知道应该回到main的哪里继续执行,所以函数的调用需要保存各种上下文
PC:PC就是一个程序计数器,它表示CPU当前正在执行/即将执行哪里的指令,还是拿图中举例,当执行main函数时,我们在哪个指令PC就指向哪个指令,当我们执行func1时,CPU做了两件事:将返回地址也就是fun1();的下一个指令压入栈,然后将func1的入口地址写入PC,然后就跳到函数中执行,当函数执行完后,从栈里弹出之前保存的返回地址,把这个地址写入PC
LR:LR就是储存返回地址的指针,但是LR只有一个,当我们有多重函数调用时,一个LR就保存不过来了,因此在多重函数调用时,我们的逻辑时,如果LR中没有返回地址,那么就将返回地址存入,如果已经有了,那么现将这个LR压入栈,再将新地址写入CPU中的LR
SP:SP保存当前栈顶的地址,在ARM64栈模型中,栈往低地址方向增长,所以当需要更多栈空间时,SP向低地址移动,SP 的变化反映了当前函数需要的栈空间。
FP:SP可能因为保存寄存器,分配局部变量等发生变化,此时如果依赖SP定位可能比较麻烦,此时可以使用FP当前栈帧的一个固定参考位置,去定位栈帧中的相关数据,并且FP还会保存上一个FP所在的位置
堆区
堆区:堆是从低地址向高地址扩展的数据结构,堆是不连续的一片内存区域,类似与链表(便于增删,不便于查询),遵循先进先出原则,堆的地址空间在iOS是以0x6开头,其空间的内存分配是动态的,堆区的分配一般在运行时分配
堆区是由程序员动态分配和释放的,如果程序员不释放,可能由操作系统回收
OC中用alloc或者使用new开辟空间创建对象
C语言中使用malloc,calloc,realloc分配的空间,需要free释放
优点:灵活方便,数据适应面广泛
缺点:需手动管理,速度慢,容易产生内存碎片
当需要访问堆中内存时,一般需要先通过对象读取到栈区的指针地址,然后通过指针地址访问堆区
全局区(.bss & .data)
全局区是编译时分配的内存空间,在iOS中一般以0x1开头,在程序运行过程中,此内存中的数据一直存在,程序结束后由系统释放,主要存放
未初始化的全局变量和静态变量,即BSS区(.bss)
已初始化的全局变量和静态变量,即数据区(.data)
其中,全局变量是指变量值可以在运行时被动态修改,而静态变量是 static 修饰的变量,包含静态局部变量和静态全局变量
常量区
常量区是 编译时分配的内存空间,在程序结束后由系统释放,主要存放 已经使用了的,且没有指向的字符串常量
字符串常量因为可能在程序中被多次使用,所以在程序运行之前就会提前分配内存
代码区
代码区是 编译时分配主要用于存放程序运行时的代码,代码会被编译成二进制存进内存的
内存管理
移动端的内存管理技术,主要有GC(垃圾回收)的标记清除算法,以及苹果公司使用的引用计数方法
相比较于 GC 标记清除算法,引用计数法可以及时地回收引用计数为 0 的对象,减少查找次数。但是,引用计数会带来循环引用的问题,比如当外部的变量强引用 Block 时,Block 也会强引用外部的变量,就会出现循环引用。我们需要通过弱引用,来解除循环引用的问题。
另外,在 ARC(自动引用计数)之前,一直都是通过 MRC(手动引用计数)这种手写大量内存管理代码的方式来管理内存,因此苹果公司开发了 ARC 技术,由编译器来完成这部分代码管理工作。但是,ARC 依然需要注意循环引用的问题。当 ARC 的内存管理代码交由编译器自动添加后,有些情况下会比手动管理内存效率低,所以对于一些内存要求较高的场景,我们还是要通过 MRC 的方式来管理、优化内存的使用
内存优化方案
Tagged Pointer
在使用Tagged Pointer前,如果声明一个NSNumber number = @10;变量,需要8个字节存指针变量number,需要16个字节存NSNumber对象,这样需要24个字节的内存空间
而使用Tagged Pointer后,NSNumber指针里存储的数据变成了:Tag + Data, 也就是将数据直接存入了指针,直接将数据10保存在指针变量number中,这样仅仅占用8个字节
但是当指针不够存储数据时,就会使用动态分配内存的方法来存储数据
dispatch_queue_t queue=dispatch_get_global_queue(0,0);for(inti=0;i<1000;i++){dispatch_async(queue,^{self.name=[NSString stringWithFormat:@"asdasdefafdfa"];});}NSLog(@"end");dispatch_queue_t queue=dispatch_get_global_queue(0,0);for(inti=0;i<1000;i++){dispatch_async(queue,^{self.name=[NSString stringWithFormat:@"abc"];});}NSLog(@"end");上面两段代码的结果是,第一段会崩溃,但第二段不会
创建多个线程来对name进行操作时,name的引用计数会不断的retain和release,此时就会出现资源竞争而崩溃,但第二段却不会崩溃,说明在Tagged Pointer下,较小的值不会调用set和get方法,可以直接修改,关于字符串什么时候是用Tagged Pointer可以见我之前的文章NSString类簇
通过源码,我们也可以比较直观的看出,Tagged Pointer类型对象是直接返回的。
在retain和release的源码中,我们都可以发现第一句就是这个,它表示Tagged Pointer根本不进引用计数逻辑,直接返回自己,不操作isa
if(slowpath(isTaggedPointer()))return(id)this;Tagged Pointer是专⻔⽤来存储⼩的对象,例如NSNumber,NSDate等。
Tagged Pointer指针的值不再是地址了,⽽是真正的值。所以,实际上它不再是⼀个对象了,它只是⼀个披着对象⽪的普通变量⽽已。所以,它的内存并不存储在堆中,也不需要malloc和free。
当指针不够存储数据时,就会使用动态分配内存的方式来存储数据。
NONPOINTER_ISA
NONPOINTER_ISA也是苹果内存优化的一种方案,用八字节来存储一个内存地址显然有些浪费,于是可以用一部分额外空间存储其他内容,isa指针第一位为1即表示使用优化的isa指针
unionisa_t{isa_t(){}isa_t(uintptr_t value):bits(value){}Class cls;uintptr_t bits;#ifdefined(ISA_BITFIELD)struct{uintptr_t nonpointer:1;uintptr_t has_assoc:1;uintptr_t has_cxx_dtor:1;uintptr_t shiftcls:44;/*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/uintptr_t magic:6;uintptr_t weakly_referenced:1;uintptr_t deallocating:1;uintptr_t has_sidetable_rc:1;uintptr_t extra_rc:8};#endif};下面解释一下isa结构体中各个变量的含义
- nonpointer:表示是否对isa指针开启指针优化 0:纯isa指针,1:不只是类对象地址,isa中包含了类信息,对象的引用计数等
- has_assoc:关联对象标志位:0没有,1存在
- has_cxx_dtor:该对象是否有C++或者Objc的析构函数,有要做析构逻辑,没有可以更快的释放对象
- shiftcls:储存类指针的值,开启指针优化的情况下,在arm架构有33位用来储存
- magic:用于调试器判断当前对象是真的对象还是没有初始化的空间
- weakly_referenced:指对象是否被指向或者曾经指向一个ARC的弱变量,没有弱引用的对象可以更快释放
- deallocating:标志对象是否正在释放内存
- has_sidetable_rc:当对象引用计数大于10时,则需要借用该变量存储进位
- extra_rc当表示该对象的引用计数值,实际上是引用计数值减1,例如,如果对象的引用计数值为10,则extra_rc为9.如果引用计数大于10则要用到has_sidetable_rc
SideTable
在NONPOINTER_ISA有两个成员变量hsa_sidetable_rc和extra_rc,当extra_r的八位不够存储引用计数时,hsa_sidetable_rc的值就会变成1,那么此时引用计数会存储在SideTable中
SideTable可以理解为一个全局的hash数组,里面存储了SideTable类型的数据,长度为64,也就是里面有64个SideTable
structSideTable{spinlock_t slock;// 自旋锁,保护这张表的读写RefcountMap refcnts;// 引用计数表(专门存那些塞不下的计数)weak_table_t weak_table;// 弱引用表(存所有 __weak 指针的地址)};slock:因为多线程可能同时操作,需要一把锁来保证线程安全。用自旋锁是因为大多数操作都很快,忙等一会儿比让出 CPU 更划算。refcnts:一个哈希表。Key 是对象的地址,Value 是引用计数的具体数值。当isa里的extra_rc溢出时,一半的计数会存到这里。weak_table:同样是个哈希表。Key 是被弱引用的对象地址,Value 是一个数组,里面装着所有指向该对象的__weak指针的地址。对象销毁时,就靠这张表把这些弱引用指针都置为nil
MRC和ARC
MRC
在MRC时代,程序员需要手动管理内存,创建一个对象时,需要在set和get方法内部添加释放对象的代码,并且在对象的dealloc里面添加dealloc里面添加释放的代码
@property(nonatomic,retain)NSString*name;-(void)setName:(NSString*)name{if(_name!=name){[_name release];// 释放旧值_name=[name retain];// 持有新值}}-(NSString*)name{return_name;}-(void)dealloc{[_name release];[_age release];[superdealloc];// 必须最后调用}ARC
在ARC环境中,我们不再像以前一样自己手动管理内存,系统帮助我们做了release或者autorelease等事情。 ARC是LLVM编译器和RunTime协作的结果。其中LLVM编译器自动生成release、reatin、autorelease的代码,像weak弱引用这些则靠RunTime在运行时释放