美文网首页
内存管理

内存管理

作者: 未来的路就在那 | 来源:发表于2017-12-20 13:39 被阅读10次

    一,堆和栈

    代码区:代码段是用来存放可执行文件的操作指令(存放函数的二进制代码),也就是说是它是可执行程序在内存种的镜像。代码段需要防止在运行时被非法修改,所以只准许读取操作,而不允许写入(修改)操作——它是不可写的。
    全局(静态)区包含下面两个分区:
           数据区:数据段用来存放可执行文件中已初始化全局变量,换句话说就是存放程序静态分配的变量和全局变量。
           BSS区:BSS段包含了程序中未初始化全局变量。
    常量区:常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,
    堆(heap)区:堆是由程序员分配和释放,用于存放进程运行中被动态分配的内存段,它大小并不固定,可动态扩张或缩减。当进程调用alloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用realse释放内存时,被释放的内存从堆中被剔除(堆被缩减),因为我们现在iOS基本都使用ARC来管理对象,所以不用我们程序员来管理,但是我们要知道这个对象存储的位置
    栈(stack)区:栈是由编译器自动分配并释放,用户存放程序临时创建的局部变量,存放函数的参数值,局部变量等。也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味这在数据段中存放变量)。除此以外在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也回被存放回栈中。由于栈的先进先出特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上将我们可以把栈看成一个临时数据寄存、交换的内存区。
    
    堆和栈的区别
    申请方式和回收方式
    栈区(stack) :由编译器自动分配并释放
    堆区(heap):由程序员分配和释放
    
    申请后系统的响应
    栈区(stack):存储每一个函数在执行的时候都会向操作系统索要资源,栈区就是函数运行时的内存,栈区中的变量由编译器负责分配和释放,内存随着函数的运行分配,随着函数的结束而释放,由系统自动完成。只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
    堆区(heap):操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
    申请大小的限制
    栈区(stack):栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,栈的大小是2M(也可能是1M,我看网上说得,我也不清楚),如果申请的空间超过栈的剩余空间时,将提示栈溢出。因此,能从栈获得的空间较小。
    堆区(stack):堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
    
    申请效率的比较
    栈区(stack):由系统自动分配,速度较快。但程序员是无法控制的。
    堆区(stack):是由alloc分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.malloc/free或new/delete大量使用后会造成内存碎片(负责动态分配内存的分配算法使得这些空闲的内存无法使用这是由于某些未分配的连续内存区域太小,以至于不能满足任意进程的内存分配请求,从而不能被进程利用的内存区域。)
    
    分配方式的比较
    栈区(stack):有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloc函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。
    堆区(stack):堆都是动态分配的,没有静态分配的堆。
    
    分配效率的比较
    栈区(stack):栈是操作系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。
    堆区(stack):堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。
    

    二,空指针、野指针和僵尸对象、内存泄露

    空指针:
        1> 没有存储任何内存地址的指针就称为空指针(NULL指针)  
        2> 空指针就是被赋值为0或nil的指针,在没有被具体初始化之前,其值为0。
    野指针:指向僵尸对象(不可用内存)的指针 给野指针发消息会报EXC_BAD_ACCESS错误,既对堆内的对象发送了dealloc消息,并销毁了,但在栈内的指针还在还指向这个堆内存的对象。
    僵尸对象:已经被销毁的对象(不能再使用的对象),在栈中指向他的指针不存在了,这个对象就无法使用了。
    

    三,assign,weak,strong,copy 详解

    强引用和弱引用
    强引用:当前对象被其他对象引用时,会执行retain操作,引用计数器+1。当retainCount=0时,该对象才会被销毁。因为我们要进行对象的内存管理,所以这是默认的引用方式。(默认是强引用)
    弱引用:当前对象的生命周期不被是否由其他对象引用限制,它本该什么时候销毁就什么时候被销毁。即使它的引用没断,但是当它的生存周期到了时就会被销毁。
    1.assign 与weak区别
    assign适用于基本数据类型,weak是适用于NSObject对象,并且是一个弱引用。
    assign其实也可以用来修饰对象。那么我们为什么不用它修饰对象呢?因为被assign修饰的对象(一般编译的时候会产生警告:Assigning retained object to unsafe property; object will be released after assignment)在释放之后,指针的地址还是存在的,也就是说指针并没有被置为nil,造成野指针。对象一般分配在堆上的某块内存,如果在后续的内存分配中,刚好分到了这块地址,程序就会崩溃掉。
    那为什么可以用assign修饰基本数据类型?因为基础数据类型一般分配在栈上,栈的内存会由系统自己自动处理,不会造成野指针。
    weak修饰的对象在释放之后,指针地址会被置为nil。所以现在一般弱引用就是用weak。(在runtime讲解了指针为什么会置为nil,)
    2.strong 与copy的区别
    strong 与copy都会使引用计数加1,但strong是两个指针指向同一个内存地址,copy会在内存里拷贝一份对象,两个指针指向不同的内存地址
    例子:假如有一个NSMutableString,现在用他给一个retain修饰 NSString赋值,那么只是将NSString指向了NSMutableString所指向的位置,并对NSMUtbaleString计数器加一,此时,如果对NSMutableString进行修改,也会导致NSString的值修改,原则上这是不允许的.  如果是copy修饰的NSString对象,在用NSMutableString给他赋值时,会进行深拷贝,及把内容也给拷贝了一份,两者指向不同的位置,即使改变了NSMutableString的值,NSString的值也不会改变.
    3.__block与__weak的区别
    __block是用来修饰一个变量,这个变量就可以在block中被修改
    __block:使用 __block修饰的变量在block代码块中会被retain(ARC下会retain,MRC下不会retain)
    __weak:使用__weak修饰的变量不会在block代码块中被retain
    同时,在ARC下,要避免block出现循环引用 __weak typedof(self)weakSelf = self;
    4. block变量定义时为什么用copy?block是放在哪里的?
    block的循环引用并不是strong导致的…在ARC环境下,系统底层也会做一次copy操作使block从栈区复制一块内存空间到堆区…所以strong和copy在对block的修饰上是没有本质区别的,只不过copy操作效率高而已
    

    四,内存管理原则
    引用计数器,自动释放池,属性参数

    基本原则
      无规矩不成方圆,在iOS开发中也存在规则来约束开发者进行内存管理,总的来讲有三点:
      1)当你通过new、alloc或copy方法创建一个对象时,它的引用计数为1,当不再使用该对象时,应该向对象发送release或者autorelease消息释放对象。
      2)当你通过其他方法获得一个对象时,如果对象引用计数为1且被设置为autorelease,则不需要执行任何释放对象的操作;
      3)如果你打算取得对象所有权,就需要保留对象并在操作完成之后释放,且必须保证retain和release的次数对等。
    
    1,引用计数
    保持对象的应用计数为1,如果大于1就需要程序员手动降为1,因为系统回收对象时,只是-1,如果-1后不是0就销毁不了。
    只要分清楚程序中的操作系统是不是帮我们做了+1的操作就行,如果系统帮我们做了+1的操作,那我们就不该在使用strong或者retain再+1,例如系统帮我们做了+1操作的有block里面的对象、代理对象、nstimer中调用[NSTimer timerWithTimeInterval:<#(NSTimeInterval)#> target:<#(nonnull id)#> selector:<#(nonnull SEL)#> userInfo:<#(nullable id)#> repeats:<#(BOOL)#>]方法和调用[NSRunLoop currentRunLoop]addTimer:<#(nonnull NSTimer *)#> forMode:<#(nonnull NSRunLoopMode)#>方法都对创建的nstimer对象做了+1
    对象已经有没有被引用采用引用计数(reference counting)的技术来进行管理:
        1)每个对象都有一个关联的整数,称为引用计数器
        2)当代码需要使用该对象时,则将对象的引用计数加1
        3)当代码结束使用该对象时,则将对象的引用计数减1
        4)当引用计数的值变为0时,表示对象没有被任何代码使用,此时对象将被释放。
    消息发送方法如下:
    1)当对象被创建(通过alloc、new或copy等方法)时,其引用计数初始值为1
        2)給对象发送retain消息,其引用计数加1
        3)給对象发送release消息,其引用计数减1
        4)当对象引用计数归0时,ObjC給对象发送dealloc消息销毁对象
    2,自动释放池
    AutoreleasePool并没有单独的结构,而是由若干个AutoreleasePoolPage以双向链表的形式组合而成。是以双向链表的结构存在栈里的,栈顶的为当前AutoreleasePool。
    手动创建的Autorelease pool当前作用域大括号结束时释放。
    在非手动添加Autorelease pool下,Autorelease对象是在当前runloop进入休眠等待前被释放的。
    原理就是对象接收到autorelease消息时,它会被添加到了当前的自动释放池中,当自动释放池被销毁时,会給池里所有的对象发送release消息。
    使用自动释放池需要注意:
    1)自动释放池实质上只是在释放的时候給池中所有对象对象发送release消息,不保证对象一定会销毁,如果自动释放池向对象发送release消息后对象的引用计数仍大于1,对象就无法销毁。
    2)自动释放池中的对象会集中同一时间释放,如果操作需要生成的对象较多占用内存空间大,可以使用多个释放池来进行优化。比如在一个循环中需要创建大量的临时变量,可以创建内部的池子来降低内存占用峰值。
    3)autorelease不会改变对象的引用计数
    
    3, 属性参数
    setter方法内存管理规则:
    retain需要使用的对象
    release之前的对象
    retain:(MRC下)系统默认重写setter和getter方法,如下,修饰OC对象,release旧值,retain新值。
    assign:直接赋值,不做任何内存管理(默认,用于非OC对象)。
    copy:release旧值,copy新值(一般用于字符串NSString*)。
    只有传入的对象和之前的不同才需要release和retain
    - (void)set:Room:(Room *)toom
    {
        //传进来的room和_room不一样的时候
        if(_room != room){
        //对旧值(当前正在使用的房间)做一次release
        [_room release];
        
        //对新房间做一次retain操作
        [room retain];
        _room = room; 
        //后两步,也可以简化成_room = [room retain]
        }
    }
    //getter方法
    - (Room*)room
    {
      return _room;
    }
    

    五,ARC内部原理
    你已经知道,ARC会自动帮你插入retainrelease语句。ARC编译器有两部分,分别是前端编译器和优化器。

    1. 前端编译器

    前端编译器会为“拥有的”每一个对象插入相应的release语句。如果对象的所有权修饰符是__strong,那么它就是被拥有的。如果在某个方法内创建了一个对象,前端编译器会在方法末尾自动插入release语句以销毁它。而类拥有的对象(实例变量/属性)会在dealloc方法内被释放。事实上,你并不需要写dealloc方法或调用父类的dealloc方法,ARC会自动帮你完成一切。此外,由编译器生成的代码甚至会比你自己写的release语句的性能还要好,因为编辑器可以作出一些假设。在ARC中,没有类可以覆盖release方法,也没有调用它的必要。ARC会通过直接使用objc_release来优化调用过程。而对于retain也是同样的方法。ARC会调用objc_retain来取代保留消息。

    2. ARC优化器

    虽然前端编译器听起来很厉害的样子,但代码中有时仍会出现几个对retainrelease的重复调用。ARC优化器负责移除多余的retainrelease语句,确保生成的代码运行速度高于手动引用计数的代码。

    相关文章

      网友评论

          本文标题:内存管理

          本文链接:https://www.haomeiwen.com/subject/tzgmwxtx.html