iOS 内存管理
引用计数
引用计数(Reference Count)是一个简单而有效的管理对象生命周期的方式。不管是Objective-C、还是Swift,其内存管理方式都是基于引用计数的。
基本概念
引用计数可以有效地管理对象生命周期。当我们为一个指针创建一个新对象的时候,它的引用计数为1。当有一个新的指针指向这个对象的时候,我们将其引用计数加1,当某个指针不在指向这个对象时,我们将引用计数减1,当对象的引用计数变为0时,说明这个对象不在被任何指针指向了,这个时候我们就可以将对象销毁,回收内存。
MRC (MannulReference Counting)
在MRC的内存管理模式下,与对变量的管理相关的方法有:retain,release和autorelease。retain和release方法操作的是引用记数,当引用记数为零时,便自动释放内存。并且可以用NSAutoreleasePool对象,对加入自动释放池(autorelease 调用)的变量进行管理,当drain时回收内存。
- retain,该方法的作用是将内存数据的所有权附给另一指针变量,引用数加1,即retainCount+= 1;
- release,该方法是释放指针变量对内存数据的所有权,引用数减1,即retainCount-= 1;
- autorelease,该方法是将该对象内存的管理放到autoreleasepool中。
//假设Number为预定义的类
Number* num = [[Number alloc] init];
Number* num2 = [num retain]; //此时引用记数+1,现为2
[num2 release]; //num2 释放对内存数据的所有权 引用记数-1,现为1;
[num release]; //num释放对内存数据的所有权 引用记数-1,现为0;
[num add:1 and 2]; //bug,此时内存已释放。
//autoreleasepool 的使用 在MRC管理模式下,我们摒弃以前的用法,NSAutoreleasePool对象的使用,新手段为@autoreleasepool
@autoreleasepool {
Number* num = [[Number alloc] init];
[num autorelease]; //由autoreleasepool来管理其内存的释放
}
对与Objective-c中属性的标识符可以总结为:
@property (nonatomic/atomic,retain/assign/copy, readonly/readwrite) Number* num;
ARC(Automatic Reference Counting)
在ARC中与内存管理有关的标识符,可以分为变量标识符和属性标识符,对于变量默认为__strong,而对于属性默认为unsafe_unretained。也存在autoreleasepool。
对于变量的标识符有:
- __strong,is the default. An object remains “alive” as long as there is a strong pointerto it.
- __weak,specifies a reference that does not keep the referenced object alive. A weakreference is set to nil when there are no strong references to the object.
- __unsafe_unretained,specifies a reference that does not keep the referenced object alive and is notset to nil when there are no strong references to the object. If the object itreferences is deallocated, the pointer is left dangling.
- __autoreleasing,is used to denote arguments that are passed by reference (id *) and areautoreleased on return,managedby Autoreleasepool.
对于变量标识符的用法:
__strong Number* num = [[Number alloc]init];
在ARC内存管理模式下,其属性的标识符存在以下几种:
@property (nonatomic/atomic, assign/retain/strong/weak/unsafe_unretained/copy,readonly/readwrite) Number* num;//默认为strong
其中assign/retain/copy与MRC下property的标识符意义相同,strong类似与retain,assign类似于 unsafe_unretained,strong/weak/unsafe_unretained与ARC下变量标识符意义相同,只是一个用于属性的标识,一个用于变量的标识(带两个下划短线__)。所列出的其他的标识符与MRC下意义相同。
- 对于assign,你可以对标量类型(如int)使用这个属性。你可以想象一个float,它不是一个对象,所以它不能retain、copy。
- 对于copy,指定应该使用对象的副本(深度复制),前一个值发送一条release消息。基本上像retain,但是没有增加引用计数,是分配一块新的内存来放置它。特别适用于NSString,如果你不想改变现有的,就用这个,因为NSMutableString,也是NSString。
对于Core Foundation与objective-cObject进行交换时,需要用到的ARC管理机制有:
- (__bridge_transfer<NSType>) op oralternatively CFBridgingRelease(op) iSUSEd to consume a retain-count of a CFTypeRef whiletransferring it over to ARC. This could also be represented by id someObj =(__bridge <NSType>) op; CFRelease(op);
- (__bridge_retained<CFType>) op oralternatively CFBridgingRetain(op) isused to hand an NSObject overto CF-land while giving it a +1 retain count. You should handle a CFTypeRefyoucreate this way the same as you would handle a result of CFStringCreateCopy().This could also be represented by CFRetain((__bridge CFType)op); CFTypeRef someTypeRef =(__bridge CFType)op;
- __bridge justcasts between pointer-land and Objective-C object-land. If you have noinclination to use the conversions above, use this one.
本质
编译时,自动添加 retain release
循环引用的常见的三种场景
- delegate
在委托问题上出现循环引用问题已经是老生常谈了,规避该问题的杀手锏也是简单到哭:声明delegate时请用assign(MRC)或者weak(ARC),千万别手贱玩一下retain或者strong,毕竟这基本逃不掉循环引用了!
- block
block在copy时都会对block内部用到的对象进行强引用(ARC)或者retainCount增1(非ARC)。在ARC与非ARC环境下对block使用不当都会引起循环引用问题,一般表现为,某个类将block作为自己的属性变量,然后该类在block的方法体里面又使用了该类本身,简单说就是self.someBlock = ^(Type var){[self dosomething];或者self.otherVar = XXX;或者_otherVar = ...};block的这种循环引用会被编译器捕捉到并及时提醒。 - NSTimer
timer都会对它的target进行retain,我们需要小心对待这个target的生命周期问题,尤其是重复性的timer。(NSTimer初始化后,self的retainCount加1。 那么,我们需要在释放这个类之前,执行[timer invalidate];否则,不会执行该类的dealloc方法。)
几种内存管理算法及特点
手动管理内存
Obj-C:retrain/release(MRC)
C:malloc/free
C++ : new/delete
优点
灵活、清晰
缺点
麻烦、容易出错
引用计数( Reference Counting )
优点
- 垃圾对象便于辨识,只要计数器为0,就可作为垃圾回收
- 执行垃圾收集任务时速度较快
缺点
- 需要单独的字段存储计数器,增加了存储空间的开销
- 每次赋值都需要更新计数器,增加了时间开销
- 无法释放循环引用的计数
以下是GC 算法
标记-擦除( Mark-Sweep )算法
标记擦除法是第一个被广泛使用的,并且可以解决循环引用问题的垃圾回收算法;
触发时机
使用标记擦除法的时候,垃圾对象并不能立即被回收,相反垃圾的回收是等到内存不够使用的时候才触发;这个时候程序的执行流程将被暂时的休眠,一旦所有的垃圾回收后,才会唤醒正常的程序执行流程。
流程
标记擦除算法包括两个阶段,在第一个节点它首先找到所有的可访问对象并进行标记,这个阶段被叫做标记阶段;第二个阶段就是扫描堆栈上的所有未标记的对象,并进行回收内存操作,这个阶段被叫做擦除节点。
实现
为了区别垃圾对象和正常对象,我们需要记录每个对象的状态;所以我们可以给每个对象添加一个布尔类型的字段marked。默认情况下,所有对象刚被创建的时候都是没有被标记的,因此字段marked的初始值为false;
优点
因为标记擦除法通过根对象跟踪所有的可访问对象,所以即使在循环引用的情况下也能正确的识别和回收垃圾对象。这个是其相对计数法的最大优势。
缺点
它需要遍历Heap中所有的对象(存活的对象在Mark阶段遍历,死亡的对象在Sweep阶段遍历)所以速度也不是十分理想。而且对垃圾进行回收以后会造成大量的内存碎片。
然而其劣势就是执行算法的时候,需要休眠中断程序的正常执行流程,特别是需要人机交互、需要满足苛刻的实时执行要求的系统。
另外一个问题就是内存碎片问题。其往往发生在已经运行过数次垃圾回收器的长时间运行的系统中。其具体的体现就是正常的对象被很多没有使用的小内存碎片隔离,其可能会导致可用内存满足所申请的内存,但是由于这些内存并不连续,导致不能正常分配内存的问题。
复制( Copy Collection )算法
复制算法将内存划分为两个区间,在任意时间点,所有动态分配的对象都只能分配在其中一个区间(称为活动区间),而另外一个区间(称为空闲区间)则是空闲的。当有效内存空间耗尽时,JVM将暂停程序运行,开启复制算法GC线程。接下来GC线程会将活动区间内的存活对象,全部复制到空闲区间,且严格按照内存地址依次排列,与此同时,GC线程将更新存活对象的内存引用地址指向新的内存地址。此时,空闲区间已经与活动区间交换,而垃圾对象现在已经全部留在了原来的活动区间,也就是现在的空闲区间。事实上,在活动区间转换为空间区间的同时,垃圾对象已经被一次性全部回收
优点
很明显,复制算法弥补了标记/清除算法中,内存布局混乱的缺点。不过与此同时,它的缺点也是相当明显的。
缺点
1、它浪费了一半的内存,这太要命了。
2、如果对象的存活率很高,我们可以极端一点,假设是100%存活,那么我们需要将所有对象都复制一遍,并将所有引用地址重置一遍。复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变的不可忽视。
所以从以上描述不难看出,复制算法要想使用,最起码对象的存活率要非常低才行,而且最重要的是,我们必须要克服50%内存的浪费。
标记-整理( Mark-Compact )算法
标记/整理算法与标记/清除算法非常相似,它也是分为两个阶段:标记和整理。
标记:它的第一个阶段与标记/清除算法是一模一样的,均是遍历GC Roots,然后将存活的对象标记。
整理:移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收。因此,第二阶段才称为整理阶段。
可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。
优点
标记/整理算法不仅可以弥补标记/清除算法当中,内存区域分散的缺点,也消除了复制算法当中,内存减半的高额代价
缺点,
标记/整理算法唯一的缺点就是效率也不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址。从效率上来说,标记/整理算法要低于复制算法。
三种算法总结
相同
- 三个算法都基于根搜索算法去判断一个对象是否应该被回收,而支撑根搜索算法可以正常工作的理论依据,就是语法中变量作用域的相关内容。因此,要想防止内存泄露,最根本的办法就是掌握好变量作用域,而不应该使用前面内存管理杂谈一章中所提到的C/C++式内存管理方式。
- 在GC线程开启时,或者说GC过程开始时,它们都要暂停应用程序(stop the world)。
不同
- 效率:复制算法>标记/整理算法>标记/清除算法(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)。
- 内存整齐度:复制算法=标记/整理算法>标记/清除算法。
- 内存利用率:标记/整理算法=标记/清除算法>复制算法。
增量收集( Incremental Collecting )算法
增量收集算法的基础仍是传统的标记-清除和复制算法。增量收集算法通过对进程间冲突的妥善处理,允许垃圾收集进程以分阶段的方式完成标记、清理或复制工作。详细分析各种增量收集算法的内部机理是一件相当繁琐的事情,在这里,读者们需要了解的仅仅是: H. G. Baker 等人的努力已经将实时垃圾收集的梦想变成了现实,我们再也不用为垃圾收集打断程序的运行而烦恼了
分代收集( Generational Collecting )算法
和大多数软件开发技术一样,统计学原理总能在技术发展的过程中起到强力催化剂的作用。 1980 年前后,善于在研究中使用统计分析知识的技术人员发现,大多数内存块的生存周期都比较短,垃圾收集器应当把更多的精力放在检查和清理新分配的内存块上。
分代收集算法通常将堆中的内存块按寿命分为两类,年老的和年轻的。垃圾收集器使用不同的收集算法或收集策略,分别处理这两类内存块,并特别地把主要工作时间花在处理年轻的内存块上。分代收集算法使垃圾收集器在有限的资源条件下,可以更为有效地工作——这种效率上的提高在今天的 Java 虚拟机中得到了最好的证明。