1、内存基础概念
1.1 物理内存 & 虚拟内存
- 物理内存(Physical Memory): 指通过物理内存条而获得的内存空间,和虚拟内存对应;主要作用是:设备运行时为操作系统和各种程序提供临时储存空间;iPhone 6 和 6 Plus 及之前都是 1G 内存、iPhone XS Max 和 11 Pro 是 4GB 内存,目前比较新的iPhone 12 Pro 是 6GB 内存;
- 虚拟内存(Virtual Memory): 是计算机系统内存管理的一种技术,为每一个进程提供了一个 一致的、私有的地址空间;其主要作用是:保护了每个进程的地址空间不会被其他进程破坏,降低内存管理的复杂性;32位设备虚拟内存大小是4GB,64位设备(5s以后的设备)是 4GB * 4GB;
- 虚拟内存 是进程运行时所有内存空间的总和,并且可能有一部分不在物理内存中;
1.2 段页式存储
- 目前,大部分通用的计算机的内存管理使用 段页式存储结构;用户程序先分段,每个段内再分页;而 页是存储的最基本单位,iOS设备的 arm64 架构后,页大小是16KB;
- 利用 逻辑地址 (段号 + 段内页号 + 页内地址) 进行地址变化,获得物理地址;这样的话,在段页式结构中,须三次访问内存才能获取数据或指令;
- 当进程访问一个虚拟内存的页时,而对应的物理内存却不存在时,会触发一次 Page Fault(缺页中断),将需要的数据 or 指令从磁盘加载到物理内存页中,建立映射关系,然后再恢复现场,程序本身是无感知的;
1.3 Swap In/Out & Page In/Out
- 磁盘内部有一个区域叫做 交换空间 (Swap Space),MMU(内存管理单元) 会将 暂时不用的内存块内容 写在交互空间上(硬盘),这就是 Swap Out;当需要时候再从 Swap Space 中读取到内存中,这就是 Swap In;Swap in和swap out的操作都是比较耗时的, 频繁的Swap in和Swap out操作非常影响系统性能;
- Page In/Out 和 Swap In/Out 概念类似,只不过Page In/Out是将 某些页 的数据写到内存/从内存写回磁盘交互区;而Swap In/Out是将 整个地址空间 的数据写到内存/从内存写回磁盘交互区;本质都是交互机制。
- macOS支持这类交换机制,但是iOS不支持;主要有两方面考虑吧:
- 移动设备的闪存读写次数有限,频繁写会降低寿命;
- 相比PC机,移动设备闪存空间有限(15年6s最小存储空间16GB、最大128GB;20年12 Pro Max最小128GB,最大521GB)
1.4 to be continued
- 通用的计算机(大型机和专用计算机不在此范围)的存储器一般设置为多层,从最靠近CPU的Cache一直到磁盘,速度越来越慢,价格也越来越便宜。
- 为了充分利用好硬件资源,Cache和Swap机制应运而生,Cache机制是一种用空间换时间的机制;而Swap机制是使用时间换空间的;
- 但是,iOS系统舍弃交换机制,取而代之的是 压缩内存(Compressed memory)机制;
2、iOS 内存管理基础概念
2.1 iOS的内存分区
从高地址到低地址各区域如下:
栈区(stack): 由编译器⾃动分配,存放 函数的参数值,局部变量的值等,作用域执行完毕之后,就会被系统收回。(栈区的地址从高到低分配)
栈是系统数据结构,对应线程/进程是唯一的。优点是快速高效,缺点时有限制,数据不灵活。【先进后出】
alloc 在堆上申请一块空间返回一个指针,这个指针在栈上,申请空间在堆上,这里指的局部变量不是对象地址,而是这个对象的指针在栈上。
栈区存储每一个函数在执行的时候都会向操作系统索要资源,栈区就是函数运行时的内存,栈区中的变量由编译器负责分配和释放,内存随着函数的运行分配,随着函数的结束而释放,由系统自动完成。
注意:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
栈是向低地址扩展的数据结构,是一块连续的内存的区域。是栈顶的地址和栈的最大容量是系统预先规定好的,如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
根据官方文档 苹果线程成本,iOS的主线程栈大小不超过1MB,OS X主线程栈最大8MB,子线程栈最大512KB。子线程在创建的时候可以更改栈的大小,子线程允许设置的最小栈大小为16 KB,并且栈大小必须为4 KB的倍数。主线程的栈大小无法修改。
由系统自动分配,速度较快,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,不会产生内存碎片。
堆区(heap): 一般由程序员分配和释放,用于存放程序运行中被动态分配的内存段;iOS 中的 Objective-C 对象存放在这里,由ARC管理;注意它与数据结构中的堆是两回事,分配方式类似于链表。(堆区的地址是从低到高分配)
堆是一种特殊的树形数据结构,每个结点都有一个值。通常我们所说的堆的数据结构,是指二叉堆。堆的特点是根结点的值最小(或最大),且根结点的两个子树也是一个堆。堆分为大根堆和小根堆,大根堆就是树的根结点大于叶子结点。
如果程序员不释放,程序结束时,可能会由操作系统回收 ,比如在iOS 中 alloc 都是存放在堆中。优点是灵活方便,数据适应面广泛,但是效率有一定降低。【顺序随意】
堆空间的分配总是动态的虽然程序结束时所有的数据空间都会被释放回系统。
堆区申请后的系统响应
- 首先应该知道操作系统有一个记录空闲内存地址的链表。
- 当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。
- 由于找到的堆结点的大小不一定正好等于申请的大小,
系统会自动的将多余的那部分重新放入空闲链表中。
申请大小的限制
堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
由 alloc 分配的内存,速度比较慢,频繁的 new/delete 势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。不过用起来最方便。
- 全局区/静态区: 由编译器分配,主要是存放全局变量 和 静态变量,程序结束后由系统释放;主要分两个区:
- BSS区: 未初始化 的全局变量 和 静态变量;
- 数据区: 已初始化 的全局变量 和 静态变量;
- 常量区: 存放的是常量,如常量字符串,程序结束后由系统释放;
- 代码区: 存放函数体的二进制代码,程序结束后由系统释放;
2.2 内存页类型:Clean 和 Dirty
内存页按照各自的 分配和使用 状态,分为 Clean
和 Dirty
两类。其中 Clean Page
是可以被回收的,Dirty Page
不能。
int *array = malloc(20000 * sizeof(int)); // 第1步
array[0] = 32 // 第2步
array[19999] = 64 // 第3步
- 第一步,申请一块长度为 80000 字节的内存空间,按照一页 16KB 来计算,就需要 6 页内存来存储。当这些内存页开辟出来的时候,它们都是
Clean
的; - 第二步,向处于第一页的内存写入数据时,第一页内存会变成
Dirty
; - 第三步,当向处于最后一页的内存写入数据时,这一页也会变成
Dirty
;
2.3 VM Region
iOS 进程中所有内存就是由许许多多的 VM Region
组成的;
VM Region 指一段连续的内存页(在虚拟地址空间里)
struct vm_region_submap_info_64 {
vm_prot_t protection; /* present access protection */
vm_prot_t max_protection; /* max avail through vm_prot */
vm_inherit_t inheritance;/* behavior of map/obj on fork */
memory_object_offset_t offset; /* offset into object/map */
unsigned int user_tag; /* user tag on map entry */
unsigned int pages_resident; /* only valid for objects */
unsigned int pages_shared_now_private; /* only for objects */
unsigned int pages_swapped_out; /* only for objects */
unsigned int pages_dirtied; /* only for objects */
unsigned int ref_count; /* obj/map mappers, etc */
unsigned short shadow_depth; /* only for obj */
unsigned char external_pager; /* only for obj */
unsigned char share_mode; /* see enumeration */
boolean_t is_submap; /* submap vs obj */
vm_behavior_t behavior; /* access behavior hint */
vm_offset_t object_id; /* obj/map name, not a handle */
unsigned short user_wired_count;
};
- VM Region包含的重要信息有:
- pages_resident: 在用的物理内存页数
- pages_dirtied: Dirty 的内存页数
- pages_swapped_out: Swapped 的内存页数(实际指的是被 Compressed Memory 页数)
- 可以通过了解
pages_dirtied
和pages_swapped_out
来了解VM Region
的真实物理内存使用。
3、iOS内存管理机制
3.1 OC的内存管理
- Objective-C 提供两种方式的内存管理方式:MRC(手动管理引用计数)和 ARC(自动引用计数)
- Objective-C 内存管理的基本原则:谁创建,谁释放,谁引用,谁管理;
- iOS 5之后提出的ARC被广泛接收,毕竟不需要管理引用计数是个很爽的事情;有了ARC,开开心心写OC;但是 ARC只管理 Objective-C 对象的内存,CoreFoundation对象、CoreGraphics对象、还有C/C++的内存分配还是需要开发者自己管理。
3.2 系统内存分类
从 iOS7 开始,系统开始采用 Compressed Memory
机制优化内存使用,内存类型可以分为三类:
- Clean Memory: 可以被释放或重建的,主要包括:
- Code
- framework,每个 framework 都有 _DATA_CONST 段,当 App 在运行时使用到了某个 framework,它所对应的 _DATA_CONST 的内存就会由
Clean
变为Dirty
。 - memory-mapped files(已被加载到内存中的文件)
Dirty Memory: 指那些被写入过数据的内存,主要包括:
- 所有堆区中的对象(Heap allocations)
- 图像解码缓冲区(Decoded image buffers)
- frameworks(framework中 _DATA 段和 _DATA_DIRTY 段)
在使用 framework 的过程中会产生Dirty Memory,使用单例或者全局初始化方法是减少Dirty Memory;这是因为单例一旦创建就不会销毁,全局初始化方法会在 class 加载时执行。
Compressed Memory
- 在内存吃紧时,系统会将不使用的内存进行压缩(Compresses unaccessed pages)
- 在需要的时候,进行解压 (Decompresses pages upon access)
- 优势:减少了不活跃内存占用;减少磁盘IO带来的损耗;压缩/解压十分迅速,能够尽可能减少 CPU 的时间开销;支持多核操作。
- 举例:当我们使用
NSDictionary
去缓存数据的时候,假设现在已经使用了 3 页内存,当不访问的时候可能会被压缩为 1 页,再次使用到时候又会解压成 3 页。
- 介绍
Clear Memory
和Dirty Memory
的Code如下:
// 堆分配的内存 Dirty Memory
NSString *str1 = [NSString stringWithString:@"Welcome!"];
// 常量字符串, 存放在一个只读数据段里面,这段内存释放后,还可以在读取重建 Clear Memory
NSString *str2 = @"Welcome!";
// 分配100M虚拟内存,当没有用时没有建立映射, Clear Memory
char *buf = malloc(100 * 1024 *1024);
for (int i = 0; i < 3 * 1024 * 1024; ++i) {
//写入数据了, Dirty Memory
buf[i] = rand();
}
注意: 在内存吃紧的情况下,释放 Clean Memory
,不能释放 Dirty Memory
,所以 Dirty Memory
的内存越多,App 的稳定性越差。
3.3 Jetsam 机制
- Jetsam 机制是操作系统为了控制内存资源过度使用而采用的一种管理机制;Jetsam 是一个独立运行的进程,会把一些 优先级不高或者占用内存过大 的App杀掉;在杀掉App后会记录一些数据信息并保存到日志。
- App优先级可以这么简单理解:前台App > 后台App; 占用内存少 > 占用内存多;
- Jetsam 产生的这些日志可以在 手机设置->隐私->分析 中找到,日志是以
JetsamEvent
开头,日志中有内存页大小(pageSize),CPU时间(cpuTime)等字段。 查看 设置->隐私->分析 中以
JetsamEvent
开头的系统日志,关注两个重要的信息:"pageSize" : 16384, // 内存页达到上限
"rpages" : 948, // App 占用的内存页数量
"reason" : "per-process-limit", // App 占用的内存超过了系统对单个 App 的内存限制。
注意: 该内存“上限”计算:pageSize rpages = 16384 948 /1024/1014 = 14.8MB,这个App应该是因优先级不高而被强杀,毕竟App内存使用上限不可能不到15MB。