美文网首页
iOS内存管理的理解

iOS内存管理的理解

作者: lymdd | 来源:发表于2020-01-10 17:45 被阅读0次

    一、为什么要内存管理

    不同的系统版本对 App 运行时占用内存的限制不同,系统版本的升级也会增加占用的内存,同时 App 功能的增多也会要求越来越多的内存。然而,移动设备的内存资源是有限的,当 App 运行时占用的内存大小超过了限制后,就会被强杀掉,从而导致用户体验被降低。所以,为了提升 App 质量,开发者要非常重视应用的内存管理问题。

    然而,移动设备的内存资源是有限的,当 App 运行时占用的内存大小超过了限制后,就会被强杀掉,从而导致用户体验被降低。所以,为了提升 App 质量,开发者要非常重视应用的内存管理问题。移动端的内存管理技术,主要有 GC(Garbage Collection,垃圾回收)的标记清除算法和苹果公司使用的引用计数方法。相比较于 GC 标记清除算法,引用计数法可以及时地回收引用计数为 0 的对象,减少查找次数。但是,引用计数会带来循环引用的问题,比如当外部的变量强引用 Block 时,Block 也会强引用外部的变量,就会出现循环引用。我们需要通过弱引用,来解除循环引用的问题。

    移动端的内存管理技术,主要有 GC(Garbage Collection,垃圾回收)的标记清除算法和苹果公司使用的引用计数方法。

    相比较于 GC 标记清除算法,引用计数法可以及时地回收引用计数为 0 的对象,减少查找次数。但是,引用计数会带来循环引用的问题,比如当外部的变量强引用 Block 时,Block 也会强引用外部的变量,就会出现循环引用。我们需要通过弱引用,来解除循环引用的问题。

    另外,在 ARC(自动引用计数)之前,一直都是通过 MRC(手动引用计数)这种手写大量内存管理代码的方式来管理内存,因此苹果公司开发了 ARC 技术,由编译器来完成这部分代码管理工作。但是,ARC 依然需要注意循环引用的问题。

    当 ARC 的内存管理代码交由编译器自动添加后,有些情况下会比手动管理内存效率低,所以对于一些内存要求较高的场景,我们还是要通过 MRC 的方式来管理、优化内存的使用。

    要想深入理解 iOS 管理内存的方式,我们就不仅仅要关注用户态接口层面,比如引用计数算法和循环引用监控技巧,还需要从管理内存的演进过程,去了解现代内存管理系统的前世今生,知其然知其所以然。

    说到内存管理的演进过程,在最开始的时候,程序是直接访问物理内存,但后来有了多程序多任务同时运行,就出现了很多问题。比如,同时运行的程序占用的总内存必须要小于实际物理内存大小。再比如,程序能够直接访问和修改物理内存,也就能够直接访问和修改其他程序所使用的物理内存,程序运行时的安全就无法保障。

    虚拟内存

    由于要解决多程序多任务同时运行的这些问题,所以增加了一个中间层来间接访问物理内存,这个中间层就是虚拟内存。

    虚拟内存通过映射,可以将虚拟地址转化成物理地址。虚拟内存会给每个程序创建一个单独的执行环境,也就是一个独立的虚拟空间,这样每个程序就只能访问自己的地址空间(Address Space),程序与程序间也就能被安全地隔离开了。32 位的地址空间是 2^32 = 4294967296 个字节,共 4GB,如果内存没有达到 4GB 时,虚拟内存比实际的物理内存要大,这会让程序感觉自己能够支配更多的内存。如同虚拟内存只供当前程序使用,操作起来和物理内存一样高效。

    有了虚拟内存这样一个中间层,极大地节省了物理内存。iOS 的共享库就是利用了这一点,只占用一份物理内存,却能够在不同应用的多份虚拟内存中,去使用同一份共享库的物理内存。

    每个程序都有自己的进程,进程的内存布局主要由代码段、数据段、栈、堆组成。程序生成的汇编代码会放在代码段。如果每个进程的内存布局都是连在一起的话,每个进程分配的空间就没法灵活变更,栈和堆没用满时就会有很多没用的空间。如果虚拟地址和物理地址的翻译内存管理单元(Memory Management Unit,MMU)只是简单地通过进程开始地址加上虚拟地址,来获取物理地址,就会造成很大的内存空间浪费。

    分段

    分段就是将进程里连在一起的代码段、数据段、栈、堆分开成独立的段,每个段内空间是连续的,段之间不连续。这样,内存的空间管理 MMU 就可以更加灵活地进行内存管理。

    那么,段和进程关系是怎么表示的呢?进程中内存地址会用前两个字节表示对应的段。比如 00 表示代码段,01 标识堆。

    段里的进程又是如何管理内存的呢?每个段大小增长的方向 Grows Positive 也需要记录,是否可读写也要记录,为的是能够更有效地管理段增长。每个段的大小不一样,在申请的内存被释放后,容易产生碎片,这样在申请新内存时,很可能就会出现所剩内存空间够用,但是却不连续,于是造成无法申请的情况。这时,就需要暂停运行进程,对段进行修改,然后再将内存拷贝到连续的地址空间中。但是,连续拷贝会耗费较多时间。

    那么,怎么才能降低内存的碎片化程度,进而提高性能呢?

    分页

    App 在运行时,大多数的时间只会使用很小部分的内存,所以我们可以使用比段粒度更小的空间管理技术,也就是分页。分页就是把地址空间切分成固定大小的单元,这样我们就不用去考虑堆和栈会具体申请多少空间,而只要考虑需要多少页就可以了。这,对于操作系统管理来说也会简单很多,只需要维护一份页表(Page Table)来记录虚拟页(Virtual Page)和物理页(Physical Page)的关系即可。

    虚拟页的前两位是 VPN(Virtual Page Number),根据页表,翻译为物理地址 PFN(Physical Frame Number)。
    虚拟页与物理页之间的映射关系,就是虚拟内存和物理内存的关系,如下图所示:


    image.png

    如图所示,多个进程虚拟页和物理页的关系通过箭头关联起来了,而页表就可以记录下箭头指向的映射关系。

    这里,我们需要注意的是,虚拟页和物理页的个数是不一样的。比如,在 64 位操作系统中使用的是 48 位寻址空间,之所以使用 48 位寻址空间,是因为推出 64 位系统时硬件还不能支持 64 位寻址空间,所以就一直延续下来了。虚拟页大小是 16K,那么虚拟页最多能有 2^48 / 2^14 = 16M 个,物理内存为 16G 对应物理页个数是 2^64 / 2^14 = 524k 个。

    维护虚拟页和物理页关系的页表会随着进程增多而变得越来越大,当页表大于寄存器大小时,就无法放到寄存器中,只能放到内存中。当要通过虚拟地址获取物理地址的时候,就要对页表进行访问翻译,而在内存中进行访问翻译的速度会比 CPU 的寄存器慢很多。

    那么,怎么加速页表翻译速度呢?

    我们知道,缓存可以加速访问。MMU 中有一个 TLB(Translation-Lookaside Buffer),可以作为缓存加速访问。所以,在访问页表前,首先检查 TLB 有没有缓存的虚拟地址对应的物理地址:如果有的话,就可以直接返回,而不用再去访问页表了;如果没有的话,就需要继续访问页表。

    每次都要访问整个列表去查找我们需要的物理地址,终归还是会影响效率,所以又引入了多级页表技术。也就是,根据一定的算法灵活分配多级页表,保证一级页表最小的内存占用。其中,一级页表对应多个二级页表,再由二级页表对应虚拟页。这样内存中只需要保存一级页表就可以,不仅减少了内存占用,而且还提高了访问效率。根据多级页表分配页表层级算法,空间占用多时,页表级别增多,访问页表层级次数也会增多,所以多级页表机制属于典型的支持时间换空间的灵活方案。

    iOS 的 XNU Mach 微内核中有很多分页器提供分页操作,比如 Freezer 分页器、VNode 分页器。还有一点需要注意的是,这些分页器不负责调度,调度都是由 Pageout 守护线程执行。

    由于移动设备的内存资源限制,虚拟分页在 iOS 系统中的控制方式更严格。移动设备的磁盘空间也不够用,因此没有使用 DRAM(动态 RAM)的方式控制内存。为了减少磁盘空间占用,iOS 采用了 Jetsam 机制来控制内存的使用。备注:DRAM 内存控制方式,是在虚拟页不命中的情况下采用磁盘来缓存。

    占用内存过多的进程会被强杀,这也就对 App 占用的内存提出了更高的要求。同时,Jetsam 机制也可以避免磁盘和内存交换带来的效率问题,因为磁盘的速度要比 DRAM 慢上几万倍。

    • 代码区
      程序代码编写的函数(其实我们编写所有的功能底层都是objc_msgSend函数)会以二进制的形式存在这里。

    • 静态区(static)
      程序启动的时候才被分配,函数里面需要用到的全局变量、static变量等存放在这。

    • 常量区(const)
      程序里面定义的字符常量存放在这。

    • 堆区(heap)
      1.程序调用(alloc、new)分配内存,保存对象实例的属性值。
      2.堆中的所有东西都是匿名的,这样不能按名字访问,而只能通过指针访问。指针存放在栈区。
      3.不保存对象的方法(对象方法是指令,保存在Stack中)

    • 栈区(stack)
      1.存放函数的参数值,局部变量的值等。
      2.对象实例在堆Heap 中分配好以后,需要在栈Stack中保存一个4字节的Heap内存地址,用 来定位该对象实例在Heap 中的位置,便于找到该对象实例。
      3.栈区读取速度快。

    注意
    1.代码区、静态区、常量区,程序运行时自动加载到内存中。堆区、栈区根据程序的代码分配内存。
    2.除了堆区的内存,系统都会自动管理,不需要开发者操心。开发者关心的是如何对堆区的内存进行管理。

    WechatIMG194.jpeg
    static NSString *myString = @"myString";
    
        NSString *str = @"abc";
        NSLog(@"str = %p",str);
    
        NSObject *obj = [[NSObject alloc] init];
        NSLog(@"obj = %p",obj);
    
        NSLog(@"myString = %p",myString);
    
    2020-01-10 14:09:22.367029+0800 RAM_Demo[88841:6905827] str = 0x10b12c208
    2020-01-10 14:09:22.367149+0800 RAM_Demo[88841:6905827] myString = 0x10b12c088
    2020-01-10 14:09:22.367098+0800 RAM_Demo[88841:6905827] obj = 0x600002cb4100
    
    

    从上面代码可以看出,str与myString在内存低地址区,而obj在内存高地址区。

    三、堆区的内存

        NSObject *obj = [[NSObject alloc] init];
        NSLog(@"obj = %p,%@",&obj,obj);
    [91063:7152129] obj = 0x7ffee681ff78,<NSObject: 0x6000008702c0>
    

    从上面我们可以看出,对象<NSObject: 0x6000008702c0>是存放在地址为0x6000008702c0的堆区,但是obj的指针是存放栈区0x7ffee681ff78位置的。

        NSObject *obj = [[NSObject alloc] init];
        NSLog(@"obj = %p,%@",&obj,obj);
        
        NSObject *temp = obj;
        obj = nil;
        
        NSLog(@"obj = %p,%@,temp = %p,%@",&obj,obj,&temp,temp);
    

    大家想想,这段代码,obj与temp会打印出来什么呢?

    [91063:7152129] obj = 0x7ffee681ff78,<NSObject: 0x6000008702c0>
    [91063:7152129] obj = 0x7ffee681ff78,(null),temp = 0x7ffee681ff70,<NSObject: 0x6000008702c0>
    

    这段代码简单理解是,先创建了一个对象<NSObject: 0x6000008702c0>,放在堆里面,obj的指针放在了栈0x7ffee681ff78里面。
    然后又定义了一个temp指针0x7ffee681ff70(可以看出来栈是高地址向低地址扩展),当然temp指针也是放在栈里面,temp指针指向了堆里的对象。当obj = nil时,obj指针指向空了,但是obj对象其实还在的,因为temp指针指向了它。

    Objective-C的内存管理本质是通过引用计数实现的,当我们新建一个新对象时候,它的引用计数+1,当一个新指针指向该对象,将引用计数+1。当指针不再指向这个对象时候,引用计数-1,当引用计数为0时,说明该对象不再被任何指针引用,将对象销毁,进而回收内存。

    所以上述例子,如果obj = nil,temp = nil,那么对象<NSObject: 0x6000008702c0>就会被释放了,因为没有指针指向它了。

    那么,obj 0x7ffee681ff78,temp 0x7ffee681ff70 什么时候被被系统回收呢?前面已经提到过了,栈区的内存系统会自动回收。代码验证如下:

    - (void)demo1 {
        
    {
        NSObject *obj = [[NSObject alloc] init];
        NSLog(@"obj = %p,%@",&obj,obj);
        
        NSObject *temp = obj;
        obj = nil;
        
        NSLog(@"obj = %p,%@,temp = %p,%@",&obj,obj,&temp,temp);
    }
    
    }
    
    - (void)demo2{
        
        {
            NSObject *obj = [[NSObject alloc] init];
            NSLog(@"obj = %p,%@",&obj,obj);
            
            NSObject *temp = obj;
            obj = nil;
            
            NSLog(@"obj = %p,%@,temp = %p,%@",&obj,obj,&temp,temp);
        }
    }
    
    [91416:7212565] obj = 0x7ffee6f70f78,<NSObject: 0x6000003203a0>
    [91416:7212565] obj = 0x7ffee6f70f78,(null),temp = 0x7ffee6f70f70,<NSObject: 0x6000003203a0>
    [91416:7212565] obj = 0x7ffee6f70f78,<NSObject: 0x600000310000>
    [91416:7212565] obj = 0x7ffee6f70f78,(null),temp = 0x7ffee6f70f70,<NSObject: 0x600000310000>
    

    可以看出demo1与demo2中obj与temp地址是一样的,这就验证了出 了作用域就被系统自动回收了。

    四、开发中要注意的内存问题

    1、定义属性copy和strong修饰的区别

    • copy修饰NSMutableArray
    @property (nonatomic,copy) NSMutableArray *muteAry;
    
    NSMutableArray *ary = [NSMutableArray arrayWithObjects:@"a",@"b",@"c",@"d", nil];
        
        self.muteAry = ary;
        
        NSLog(@"ary = %@",ary);
        NSLog(@"muteAry = %@",self.muteAry);
        
        [ary removeLastObject];
        
        NSLog(@"ary = %@",ary);
        NSLog(@"muteAry = %@",self.muteAry);
    
     RAM_Demo[92198:7332911] ary = (
        a,
        b,
        c,
        d
    ),0x600000a45800
     RAM_Demo[92198:7332911] muteAry = (
        a,
        b,
        c,
        d
    ),0x600000a44e40
     RAM_Demo[92198:7332911] ary = (
        a,
        b,
        c
    ),0x600000a45800
     RAM_Demo[92198:7332911] muteAry = (
        a,
        b,
        c,
        d
    ),0x600000a44e40
    

    可以看到,用copy修饰NSMutableArray,当 self.muteAry = ary 时,系统会复制一个对象0x600000a44e40,ary的操作对self.muteAry没有影响。

    • strong修饰NSMutableArray
    @property (nonatomic,strong) NSMutableArray *muteAry;
    NSMutableArray *ary = [NSMutableArray arrayWithObjects:@"a",@"b",@"c",@"d", nil];
        
        self.muteAry = ary;
        
        NSLog(@"ary = %@",ary);
        NSLog(@"muteAry = %@",self.muteAry);
        
        [ary removeLastObject];
        
        NSLog(@"ary = %@",ary);
        NSLog(@"muteAry = %@",self.muteAry);
    
     RAM_Demo[92284:7346529] ary = (
        a,
        b,
        c,
        d
    ),0x600000154f00
     RAM_Demo[92284:7346529] muteAry = (
        a,
        b,
        c,
        d
    ),0x600000154f00
     RAM_Demo[92284:7346529] ary = (
        a,
        b,
        c
    ),0x600000154f00
    RAM_Demo[92284:7346529] muteAry = (
        a,
        b,
        c
    ),0x600000154f00
    
    

    可以看到,如果用strong修饰NSMutableArray,当 self.muteAry = ary 时,系统并没有创建新的对象,如果对ary操作,就会直接影响到self.muteAry的值。

    如果想直接拷贝一份,可以这样写 self.muteAry = ary.copy;这样系统就会创建一份新的对象。

    @property (nonatomic,strong) NSMutableArray *muteAry;
        NSMutableArray *ary = [NSMutableArray arrayWithObjects:@"a",@"b",@"c",@"d", nil];
        
        self.muteAry = ary.copy;
        
        NSLog(@"ary = %@,%p",ary,ary);
        NSLog(@"muteAry = %@,%p",self.muteAry,self.muteAry);
        
        [ary removeLastObject];
        
        NSLog(@"ary = %@,%p",ary,ary);
        NSLog(@"muteAry = %@,%p",self.muteAry,self.muteAry);
        
    }
     ary = (
        a,
        b,
        c,
        d
    ),0x600000cdd710
     muteAry = (
        a,
        b,
        c,
        d
    ),0x600000cdd740
     ary = (
        a,
        b,
        c
    ),0x600000cdd710
     muteAry = (
        a,
        b,
        c,
        d
    ),0x600000cdd740
    

    2、使用Block时要注意的地方

    未完待续

    相关文章

      网友评论

          本文标题:iOS内存管理的理解

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