内存管理

简介

  • 内存的组成

    内存空间在逻辑上分为三部分:代码区、静态数据区和动态数据区,动态数据区又分为栈区和堆区。

    代码区:存储方法体的二进制代码。高级调度(作业调度)、中级调度(内存调度)、低级调度(进程调度)控制代码区执行代码的切换

    静态数据区:存储全局变量、静态变量、常量,常量包括final修饰的常量和String常量。系统自动分配和回收

    栈区:存储运行方法的形参、局部变量、返回值。由系统自动分配和回收。
    例如 int method(int a){int b;}栈中存储参数a、局部变量b、返回值temp。

    堆区:new一个对象的引用或地址存储在栈区,指向该对象存储在堆区中的真实数据。由程序员分配和回收(Java中由JVM虚拟机的垃圾回收机制自动回收)。
    例如 Class Student{int num; int age;} main方法中Student stu = new Student();分配堆区空间中存储的该对象的num、age,变量stu存储在栈中,里面的值是对应堆区空间的引用或地址。

  • 内存中的堆栈和数据结构堆栈

    内存中的堆栈是真实存在的物理区,数据结构中的堆栈是抽象的数据存储结构。

    数据结构中的堆栈

    栈:是一种连续存储的数据结构,特点是存储的数据先进后出。

    堆:是一棵完全二叉树结构,特点是父节点的值大于(小于)两个子节点的值(分别称为大顶堆和小顶堆)。它常用于管理算法执行过程中的信息,应用场景包括堆排序,优先队列等。

  • 内存分配中的栈和堆

    堆栈空间分配

    栈(操作系统):由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。

    堆(操作系统): 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收,分配方式倒是类似于链表。

    堆栈缓存方式

    栈使用的是一级缓存, 他们通常都是被调用时处于存储空间中,调用完毕立即释放。

    堆则是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对来得低一些。

引用计数

栈里面存放的是值类型,堆里面存放的是对象类型。对象的引用计数是在堆内存中操作的

引用计数简介

引用计数(Reference Count)是一个简单而有效的管理对象生命周期的方式。当我们创建一个新对象的时候,它的引用计数为 1,当有一个新的指针指向这个对象时,我们将其引用计数加 1,当某个指针不再指向这个对象是,我们将其引用计数减 1,当对象的引用计数变为 0 时,说明这个对象不再被任何指针指向了,这个时候我们就可以将对象销毁,回收内存。由于引用计数简单有效,除了 Objective-C 和 Swift 语言外,微软的 COM(Component Object Model )、C++11(C++11 提供了基于引用计数的智能指针 share_prt)等语言也提供了基于引用计数的内存管理方式。

启动手工管理引用计数模式。 -fno-objc-arc
使用ARC 默认 -fobjc-arc

ARC指的是编译器在编译时会帮我们自动插入,包括 retain、release、copy、autorelease、autoreleasepool

引用计数的这种管理方式类似于文件系统里面的硬链接。在 Linux 文件系统中,我们用 ln 命令可以创建一个硬链接(相当于我们这里的 retain),当删除一个文件时(相当于我们这里的 release),系统调用会检查文件的 link count 值,如果大于 1,则不会回收文件所占用的磁盘区域。直到最后一次删除前,系统发现 link count 值为 1,则系统才会执行直正的删除操作,把文件所占用的磁盘区域标记成未用。

为什么需要引用计数

问题:对象 A 生成了一个对象 M,将对象 M 作为参数传递给对象B。

  • 第一种暴力方法

    • 按照一般内存管理的原则是 “谁申请谁释放”,对象 A 在调用完对象 B 之后,马上就销毁参数对象 M,然后对象 B 需要将参数另外复制一份,生成另一个对象 M2,然后自己管理对象 M2 的生命期

      这种做法有一个很大的问题,就是它带来了更多的内存申请、复制、释放的工作。本来一个可以复用的对象,因为不方便管理它的生命期,就简单的把它销毁,又重新构造一份一样的,实在太影响性能

  • 第二种配合方法

    • 对象 A 在构造完对象 M 之后,始终不销毁对象 M,由对象 B 来完成对象 M 的销毁工作。如果对象 B 需要长时间使用对象 M,它就不销毁它,如果只是临时用一下,则可以用完后马上销毁。

      这种做法看似很好地解决了对象复制的问题,但是它强烈依赖于 AB 两个对象的配合,代码维护者需要明确地记住这种编程约定,由于对象 M 的申请是在对象 A 中,释放在对象 B 中,使得它的内存管理代码分散在不同对象中,管理起来也非常费劲,这种方式带来的复杂性更大,更不可取

  • 引用计数方法

  • 正常情况下,当一段代码需要访问某个对象时,该对象的引用的计数加1;当这段代码不再访问该对象时,该对象的引用计数减1,表示这段代码不再访问该对象;当对象的引用计数为0时,表明程序已经不再需要该对象,系统就会回收该对象所占用的内存。

     - 当程序调用方法名以alloc、new、copy、mutableCopy开头的方法来创建对象时,该对象的引用计数加1。
     - 程序调用对象的retain方法时,该对象的引用计数加1。
     - 程序调用对象的release方法时,该对象的引用计数减1。
    
  • NSObject 中提供了有关引用计数的如下方法:
    - retain:将该对象的引用计数器加1。
    - release:将该对象的引用计数器减1。
    - autorelease:不改变该对象的引用计数器的值,只是将对象添加到自动释放池中。
    - retainCount:返回该对象的引用计数的值。

    引用计数这种管理内存的方式虽然很简单,但是有一个比较大的瑕疵,即它不能很好的解决循环引用问题。如下图所示:对象 A 和对象 B,相互引用了对方作为自己的成员变量,只有当自己销毁时,才会将成员变量的引用计数减 1。因为对象 A 的销毁依赖于对象 B 销毁,而对象 B 的销毁与依赖于对象 A 的销毁,这样就造成了我们称之为循环引用(Reference Cycle)的问题,这两个对象即使在外界已经没有任何指针能够访问到它们了,它们也无法被释放。

    主动断开循环引用, 我明确知道这里会存在循环引用,在合理的位置主动断开环中的一个引用,使得对象得以回收

    使用弱引用, 弱引用虽然持有对象,但是并不增加引用计数,这样就避免了循环引用的产生

    系统对于每一个有弱引用的对象,都维护一个表来记录它所有的弱引用的指针地址。这样,当一个对象的引用计数为 0 时,系统就通过这张表,找到所有的弱引用指针,继而把它们都置成 nil。 相当于主动断开循环引用

ARC自动管理计数

所有权修饰符

Objective-C编程中为了处理对象,可将变量类型定义为id类型或各种对象类型。 ARC中,id类型和对象类其类型必须附加所有权修饰符。

__strong修饰符

__weak修饰符

__unsafe_unretained修饰符

__autoreleasing修饰符

  • __strong修饰符

    __strong修饰符是id类型和对象类型默认的所有权修饰符。也就是说,不写修饰符的话,默认对象前面被附加了__strong所有权修饰符。

    __strong修饰符的变量obj在超出其变量作用域时,即在该变量被废弃时,会释放其被赋予的对象。

    __strong修饰符表示对对象的“强引用”。持有强引用的变量在超出其作用域时被废弃,随着强引用的失效,引用的对象会随之释放。

  • __weak修饰符

    通过__strong修饰符并不能完美的进行内存管理,这里会发生“循环引用”的问题。使用__weak修饰符还有另外一个优点。在持有某对象的弱引用时,若该对象被废弃,则此弱引用将自动失效且处于nil赋值的状态(空弱引用)。

  • __unsafe_unretained修饰符

    __unsafe_unretained修饰符是不安全的修饰符,尽管ARC式的内存管理是编译器的工作,但附有

    __unsafe_unretained修饰符的变量不属于编译器的内存管理对象。

    __unsafe_unretained和
    __weak一样不能持有对象。

  • __autoreleasing修饰符

ARC规则

在ARC有效的情况下编译源代码,必须遵守一定的规则。

  • 不能使用retain/release/retainCount/autorelease
  • 不要显式调用dealloc
  • 须遵守内存管理的方法命名规则. 符合命名规则

ARC内存的泄露和检测

你可能感兴趣的:(内存管理)