iOS内存管理的理解

2020-01-10  本文已影响0人  lymdd

一、为什么要内存管理

不同的系统版本对 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 慢上几万倍。

注意
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修饰的区别

@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没有影响。

@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时要注意的地方

未完待续

上一篇下一篇

猜你喜欢

热点阅读