内存管理面试题
autorelease 对象会在什么时候释放?
- 使用@autoreleasepool
会在大括号结束时释放
struct __AtAutoreleasePool {
__AtAutoreleasePool() { // 构造函数,在创建结构体的时候调用
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
~__AtAutoreleasePool() { // 析构函数,在结构体销毁的时候调用
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};
{ } 是作用域,
{ } 里会调用构造函数间接的会调objc_autoreleasePoolPush函数:
{ } 外调用 析构函数调用objc_autoreleasePoolPop函数
- 不使用 @autoreleasepool
这个会由系统自动释放,释放时机是在当前 runloop 结束时释放,因为系统会自动为每个
runloop 执行自动释放池的 push 和 pop 操作
Runloop和Autorelease
iOS在主线程的Runloop中注册了2个Observer
- 第1个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush()
- 第2个Observer <1> 监听了kCFRunLoopBeforeWaiting事件,会调用
objc_autoreleasePoolPop()、objc_autoreleasePoolPush() - <2> 监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()
什么对象会加入Autoreleasepool中
- 使用alloc、new、copy、mutableCopy的方法进行初始化时,由系统管理对象,在适当的位置release。
- 使用array会自动将返回值的对象注册到Autoreleasepool。
- __weak修饰的对象,为了保证在引用时不被废弃,会注册到Autoreleasepool中。
- id的指针或对象的指针,在没有显示指定时会被注册到Autoleasepool中。
谈谈你对自动释放池的理解
自动释放池是oc提供的一种自动回收的机制,具有延迟释放的特性,即当我们创建了一个对象,并把他加入到了自动释放池中时,他不会立即被释放,会等到一次runloop结束或者作用域超出{}或者超出[pool release]之后再被释放。
系统就是通过@autoreleasepool {}这种方式来为我们创建自动释放池的,
一个线程对应一个runloop,系统会为每一个runloop隐式的创建一个自动释放池,
所有的autoreleasePool构成一个栈式结构,在每个runloop结束时,
当前栈顶的autoreleasePool会被销毁,而且会对其中的每一个对象做一次release(严格来说,是你对这个对象做了几次autorelease就会做几次release,不一定是一次).
特别指出,使用容器的block版本的枚举器的时候,系统会自动添加一个autoreleasePool
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
// 这里被一个局部@autoreleasepool包围着
}];
自动释放池的原理
AutoreleasePoolPage:每一个自动释放池没有单独的结构,每一个autorealeasePool对象都是由若干个autoreleasePoolPage通过双向链表连接而成,类的定义如下
class AutoreleasePoolPage
{
magic_t const magic;
id *next;//指向栈顶最新被添加进来的autorelease对象的下一个位置
pthread_t const thread;//指向当前线程
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t
}
- autoreleasePool是按照线程一一对应的,
- autoreleasePoolPage会开辟4096字节空间,除了上面的实例变量所占的空间,剩余的空间全部用来存储autorelease对象的地址
- id *next:指向栈顶最新被添加进来的autorelease对象的下一个位置
- 一个autoreleasePoolPage空间被占满时,会创建一个新的autoreleasePoolPage对象,后来的对象添加在在新
autoreleasePoolPage中 - 哨兵对象:本质是一个值为nil的对象,由被定义在* NSAutoreleasePool类中的_token指针来保存。
hotPage:最新创建的AutoreleasePoolPage.
系统通过一个栈来管理所有的自动释放池,每当创建了一个新的自动释放池,系统就会把它压入栈顶,并且传入一个哨兵对象,将哨兵对象插入hotPage,这里分三种情况
- 若hotPage未满,则直接插入哨兵对象,
- 要是满了,新建一个NSAutoreleasePoolPage,并将其作为hotPage,然后将哨兵对象插入
- 如果没有NSAutoreleasePoolPage,则新建一个NSAutoreleasePoolPage,并将其作为hotPage,插入哨兵对象,注意。这里的hotPage是没有父节点的。
每当有一个自动释放池要被释放的时候,哨兵对象就会作为参数被传入,找到该哨兵对象所在的位置后,将所有晚于哨兵对象的autorelease弹出,并对他们做一次release,然后将next指针一到合适的位置。
自动释放池的应用场景
- 对象作为函数返回值
当一个对象要作为函数返回值的时候,因为要遵循谁申请谁释放的思想,所以应该在返回之前释放,但要是返回之前释放了,就会造成野指针错误,但是要是不释放,那么就违背了谁申请谁释放的原则,所以就可以使用autorelease延迟释放的特性,将其在返回之前做一次autorelease,加入到自动释放池中,保证可以被返回,一次runloop之>>后系统会帮我们释放他
- 临时生成大量对象,一定要将自动释放池放在for循环里面,要释放在外面,就会因为大量对象得不到及时释放,而造成内存紧张,最后程序意外退出
介绍下内存的几大区域?
栈区(stack)
由编译器自动分配并释放,存放函数的参数值,局部变量等。栈是系统数据结构,对应线程/进程是唯一的。优点是快速高效,缺点时有限制,数据不灵活。[先进后出]
堆区(heap)
由程序员分配和释放,如果程序员不释放,程序结束时,可能会由操作系统回收 ,比如在ios 中 alloc 都是存放在堆中。
优点是灵活方便,数据适应面广泛,但是效率有一定降低。
全局区(静态区)
(static) 全局变量和静态变量的存储是放在一起的,
初始化的全局变量和静态变量存放在一块区域,
未初始化的全局变量和静态变量在相邻的另一块区域,
程序结束后有系统释放。
文字常量区
存放常量字符串,程序结束后由系统释放;
代码区
存放函数的二进制代码
栈区 (stack [stæk]): 由编译器自动分配释放
- 局部变量是保存在栈区的
- 方法调用的实参也是保存在栈区的
堆区 (heap [hiːp]): 由程序员分配释放
代码段:程序结束后由系统释放
程序编译链接 后的二进制可执行代码
申请后的系统是内存如何响应的?
栈:存储每一个函数在执行的时候都会向操作系统索要资源,栈区就是函数运行时的内存,栈区中的变量由编译器负责分配和释放,内存随着函数的运行分配,随着函数的结束而释放,由系统自动完成。
注意:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆:
- 首先应该知道操作系统有一个记录空闲内存地址的链表。
- 当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。