[关闭]
@qidiandasheng 2020-07-24T11:35:17.000000Z 字数 11394 阅读 3061

iOS内存分配:虚拟内存

计算机系统


物理内存

一个设备的 RAM 大小。以下是维基百科上的资料:

v2-8bb11ff2155b56b8bd7a73bb49b7f7bb_1440w.jpg-128kB

简单来说,iPhone 8(不包括 plus) 和 iPhone 7(不包括 plus)及之前都是 2G 内存,iPhone 6 和 6 plus 及之前都是 1G 内存。

虚拟内存(VM for Virtual Memory)

什么是虚拟内存

当我们向系统申请内存时,系统并不会直接返回物理内存的地址,而是返回一个虚拟内存地址。从系统角度来说,每个进程都有一个自己私有的相同大小的虚拟内存空间。对于32位设备来说是4GB,而64位设备(5s以后的设备)是 18EB(1EB = 1000PB, 1PB = 1000TB),映射到物理内存空间。

只有当进程开始使用申请到的虚拟内存时,系统才会将虚拟地址映射到物理地址上,从而让程序使用真实的物理内存。

导出图片Wed Jul 22 2020 13_53_48 GMT+0800 (中国标准时间).png-204.5kB

获取App申请到的所有虚拟内存:

  1. - (int64_t)memoryVirtualSize {
  2. struct task_basic_info info;
  3. mach_msg_type_number_t size = (sizeof(task_basic_info_data_t) / sizeof(natural_t));
  4. kern_return_t ret = task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t)&info, &size);
  5. if (ret != KERN_SUCCESS) {
  6. return 0;
  7. }
  8. return info.virtual_size;
  9. }

Clean Memory

可以简单理解为能够被写入数据的干净内存。对开发者而言是read-only,而iOS系统可以写入或移除。

注意:如果通过文件内存映射机制memory mapped file载入内存的,可以先清除这部分内存占用,需要的时候再从文件载入到内存。所以是Clean Memory

Dirty Memory

主要强调不可被重复使用的内存。对开发者而言,已经写入数据。

iOS中的内存警告,只会释放clean memory。因为iOS认为dirty memory有数据,不能清理。所以,应尽量避免dirty memory过大。

Clean和Dirty示例

  1. int *array = malloc(20000 * sizeof(int)); // 第1步
  2. array[0] = 32 // 第2步
  3. array[19999] = 64 // 第3步

Resident Memory

已经被映射到虚拟内存中的物理内存。存在一些“非代码执行开销”,如系统和应用二进制加载的内存。

  1. Resident Memory = Dirty Memory + Clean Memory that loaded in pysical memory

这里存在两种Resident Memory,系统的(dyld_shared_cache,即动态库共享缓存)和我们APP的,下面的代码得到的是我们APP的。

获取App消耗的Resident Memory:

  1. #import <mach/mach.h>
  2. - (int64_t)memoryResidentSize {
  3. struct task_basic_info info;
  4. mach_msg_type_number_t size = sizeof(task_basic_info_data_t) / sizeof(natural_t);
  5. kern_return_t ret = task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t)&info, &size);
  6. if (ret != KERN_SUCCESS) {
  7. return 0;
  8. }
  9. return info.resident_size;
  10. }

注意: Resident Memory包含Memory Footprint

Memory Footprint

App消耗的实际物理内存,苹果推荐用 footprint 命令来查看一个应用进程的内存占用。

我们会发现这个跟我们在 Instruments 里面看到的内存大小不一样,有时候甚至差别很大。其实Footprint主要就是Dirty部分,也就是我们可以控制优化的部分。Xcode Navigator里记录的大致也差不多是这个值。

获取App的Footprint:

  1. #import <mach/mach.h>
  2. - (int64_t)memoryPhysFootprint {
  3. task_vm_info_data_t vmInfo;
  4. mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
  5. kern_return_t ret = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t)&vmInfo, &count);
  6. if (ret != KERN_SUCCESS) {
  7. return 0;
  8. }
  9. return vmInfo.phys_footprint;
  10. }

XNU中Jetsam判断内存过大,使用的也是phys_footprint,而非resident size

内存测量

代码

  1. - (void)viewDidLoad {
  2. [self logMemorySize];
  3. void *memBlock = malloc(10 * 1024 * 1024);
  4. [self logMemorySize];
  5. memset(memBlock, 0, 10 * 1024 * 1024);
  6. [self logMemorySize];
  7. }

初始值

类型 内存值 分析
resident 88.36 App消耗的内存
footprint 11.52 实际物理内存(Dirty部分)
VM 4941 App分配的虚拟内存
Xcode Navigator 11.5 footprint + 调试需要

申请虚拟内存后

类型 内存值 分析
resident 88.36 App消耗的内存
footprint 11.52 实际物理内存(Dirty部分)
VM 4951 App分配的虚拟内存
Xcode Navigator 11.5 footprint + 调试需要

映射物理内存后

类型 内存值 分析
resident 98.36 App消耗的内存
footprint 21.52 实际物理内存(Dirty部分)
VM 4951 App分配的虚拟内存
Xcode Navigator 21.5 footprint + 调试需要

Instruments查看

运行Allocations模块,添加VM Tracker。下图是映射物理内存之后:

截屏2020-07-23 上午11.33.12.png-689.4kB

其中系统的Resident主要就是dyld_shared_cache(动态库共享缓存),所有APP的虚拟内存对动态库的物理内存映射都是映射到这块的,我们运行不同的app查看dyld_shared_cache的地址都是一样的:

截屏2020-07-23 下午12.26.48.png-361.9kB

总结:
All Resident = 系统Resident + APP Resident
APP Resident = Memory Footprint + Clean Memory that loaded in pysical memory(_TEXT + _OBJC_RO + Other)

内存不够怎么办

进程A和B都拥有1到4的虚拟内存。系统通过虚拟内存到物理内存的映射,让A和B都可以使用到物理内存。上图中物理内存是充足的,但是如果A占用了大部分内存,B想要使用物理内存的时候物理内存却不够该怎么办呢?

OSX

在OSX上系统会将不活跃的内存块写入硬盘,一般称之为Swapped out

由于虚拟内存的空间远远大于物理内存,在任意一个时间点,虚拟内存中的一个页并不一定总是在物理内存中,而是可能被暂时存到了磁盘上,这样物理内存便可以暂时释放这部分空间,供优先级更高的任务使用,因此磁盘可以作为 backing store 以扩展物理内存(MacOS 中有,iOS 没有)。

iOS

由于闪存容量和读写寿命的限制,iOS 上没有Disk swap机制,取而代之使用 Compressed memory。等压缩后内存也不够用后,iOS上则会通知App,让App清理内存,也就是我们熟知的Memory Warning

苹果最初只是公开了从 OS X Mavericks 开始使用 Compressed memory 技术,但 iOS 系统也从 iOS 7 开始悄悄地使用。该技术在内存紧张时能够将最近使用过的内存占用压缩至原有大小的一半以下,并且能够在需要时解压复用。它在节省内存的同时提高了系统的响应速度,其特点可以归结为:

Swap In/Out & Page In/Out

内存分页

系统会对虚拟内存和物理内存进行分页,虚拟内存到物理内存的映射都是以页为最小粒度的。

也就是说内存管理、映射中的基本单位是页,一页的大小是 4kb(早期设备)或者 16kb(A7 芯片及以后)。

v2-79ef155715919086cf5b93c0957efc68_1440w.jpg-26.6kB

分页解决了什么问题

内存分页的状态

系统将内存页分为三个状态:

当可用的内存页降低到一定的阀值时,系统就会采取低内存应对措施,在OSX中,系统会将非活跃内存页交换到硬盘上,而在iOS中,则会触发Memory Warning,如果你的App没有处理低内存警告并且还在后台占用太多内存,则有可能被杀掉。

VM Region/VM Object

一个 VM Region 是指一段连续的内存页(在虚拟地址空间里),这些页拥有相同的属性(如读写权限、是否是 wired,也就是是否能被 page out)。

每个 VM Region 对应一个数据结构,名为 VM Object。Object 会记录这个 Region 内存的属性:

查看VM Region

编写代码

创建工程运行以下代码,main图片分辨率为750 × 1624,Labrador图片分辨率600 × 704

  1. #import "CustomObject.h"
  2. @implementation CustomObject{
  3. long a[200];
  4. }
  5. @end
  1. - (void)viewDidLoad {
  2. [super viewDidLoad];
  3. // Do any additional setup after loading the view.
  4. self.myObjsArr = [NSMutableArray array];
  5. for (int i=0; i<10000; i++) {
  6. CustomObject *obj = [CustomObject new];
  7. [self.myObjsArr addObject:obj];
  8. }
  9. UIImageView *showImageView = [[UIImageView alloc] initWithFrame:self.view.bounds];
  10. [showImageView setImage:[UIImage imageNamed:@"main"]];
  11. [self.view addSubview:showImageView];
  12. UIImageView *showImageView2 = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 325, 412)];
  13. [showImageView2 setImage:[UIImage imageNamed:@"Labrador.JPG"]];
  14. [self.view addSubview:showImageView2];
  15. }

查看内存地址

运行Instruments,选择Allocation模版,右上角选择添加VM Track

截屏2020-07-22 下午3.06.14.png-1440.6kB

Instruments启动app,Allocation里查看到我们创建的CustomObjectImage IO的内存,点击右箭头能看到对应的内存地址:
截屏2020-07-22 下午3.39.42.png-490.4kB

截屏2020-07-22 下午3.43.03.png-409.5kB

查看VM Region

我们切换到最底下的VM Track,将模式调整为Regions Map,就能看到VM Region的一个列表了,其中每一行代表一个VM Region,我们能看到每一个VM Region的大小都是不一样的。

截屏2020-07-22 下午3.45.39.png-1356.5kB

我们能看到每张图片解码缓存Image IO占用一个Region,而我们的CustomObject都在一个MALLOC_SMALL类型的Region里。

VM Region类型

我们能看到VM Region有很多不同的类型,这里我们举几个例子:

VM Region Size

我们在VM Track中可以看到,一个VM Region有4种size。

所以一般来说app运行过程中在堆上动态分配的内存页都是Dirty的,加载动态库或者文件内存映射产生的内存页则是非Dirty的。综上,我们可以总结出:

Virtual Size >= Resident Size + Swapped Size >= Dirty Size + Swapped Size,

活跃状态:
截屏2020-07-22 下午4.23.30.png-381.7kB
非活跃状态:
截屏2020-07-22 下午4.22.40.png-317.6kB

malloc 和 calloc

我们除了使用NSObject的alloc分配内存外,还可以使用c的函数malloc进行内存分配。malloc的内存分配当然也是先分配虚拟内存,然后使用的时候再映射到物理内存,不过malloc有一个缺陷,必须配合memset将内存区中所有的值设置为0。这样就导致了一个问题,malloc出一块内存区域时,系统并没有分配物理内存。然而,调用memset后,系统将会把malloc出的所有虚拟内存关联到物理内存上,因为你访问了所有内存区域。

malloc_zone_t 和 NSZone

相信大家对NSZone并不陌生,allocWithZone或者copyWithZone这2个方法大家应该也经常见到。那么Zone究竟是什么呢?Zone可以被理解为一组内存块,在某个Zone里分配的内存块,会随着这个Zone的销毁而销毁,所以Zone可以加速大量小内存块的集体销毁。不过NSZone实际上已经被苹果抛弃。你可以创建自己的NSZone,然后使用allocWithZone将你的OC对象在这个NSZone上分配,但是你的对象还是会被分配在默认的NSZone里。我们可以用heap工具查看进程的Zone分布情况。首先使用下面的代码让CustomObject使用新的NSZone

  1. void allocCustomObjectsWithCustomNSZone() {
  2. static NSMutableSet *objs = nil;
  3. if (objs == nil) { objs = [NSMutableSet new]; }
  4. NSZone *customZone = NSCreateZone(1024, 1024, YES);
  5. NSSetZoneName(customZone, @"Custom Object Zone");
  6. for (int i = 0; i < 1000; ++i) {
  7. CustomObject *obj = [CustomObject allocWithZone:customZone];
  8. [objs addObject:obj];
  9. }
  10. }

代码创建了1000个CustomObject对象,并且尝试使用新建的Zone。我们用heap工具看看结果。首先使用Activity Monitor找到进程的PID,在命令行中执行

  1. heap PID

执行的结果大致如下:

  1. ......
  2. Process 25073: 3 zones
  3. Zone DefaultMallocZone_0x1004c9000: Overall size: 196992KB; 13993 nodes malloced for 160779KB (81% of capacity); largest unused: [0x102800000-171072KB]
  4. Zone Custom Object Zone_0x1004fe000: Overall size: 1024KB; 1 nodes malloced for 1KB (0% of capacity); largest unused: [0x102200000-1024KB]
  5. Zone GFXMallocZone_0x1004d8000: Overall size: 0KB
  6. All zones: 13994 nodes malloced - 160779KB
  7. Zone DefaultMallocZone_0x1004c9000: 13993 nodes - Sizes: 160KB[1000] 64.5KB[1] 16.5KB[1] 13.5KB[1] 4.5KB[3] 2KB[3] 1.5KB[12] 1KB[1] 704[1] 576[13] 528[4] 512[2] 480[1] 464[1] 448[2] 432[1] 400[1] 384[2] 368[1] 352[1] 336[2] 320[1] 272[8] 256[1] 240[4] 208[10] 192[5] 176[3] 160[5] 144[28] 128[48] 112[43] 96[83] 80[519] 64[3044] 48[5415] 32[3640] 16[82]
  8. Zone Custom Object Zone_0x1004fe000: 1 nodes - Sizes: 32[1]
  9. Zone GFXMallocZone_0x1004d8000: 0 nodes
  10. All zones: 13994 nodes malloced - Sizes: 160KB[1000] 64.5KB[1] 16.5KB[1] 13.5KB[1] 4.5KB[3] 2KB[3] 1.5KB[12] 1KB[1] 704[1] 576[13] 528[4] 512[2] 480[1] 464[1] 448[2] 432[1] 400[1] 384[2] 368[1] 352[1] 336[2] 320[1] 272[8] 256[1] 240[4] 208[10] 192[5] 176[3] 160[5] 144[28] 128[48] 112[43] 96[83] 80[519] 64[3044] 48[5415] 32[3641] 16[82]
  11. Found 523 ObjC classes
  12. Found 56 CFTypes
  13. -----------------------------------------------------------------------
  14. Zone DefaultMallocZone_0x1004c9000: 13993 nodes (164637440 bytes)
  15. COUNT BYTES AVG CLASS_NAME TYPE BINARY
  16. ===== ===== === ========== ==== ======
  17. 12771 779136 61.0 non-object
  18. 1000 163840000 163840.0 CustomObject ObjC VMResearch
  19. 49 2864 58.4 CFString ObjC CoreFoundation
  20. 21 1344 64.0 pthread_mutex_t C libpthread.dylib
  21. 20 1280 64.0 CFDictionary ObjC CoreFoundation
  22. 18 2368 131.6 CFDictionary (Value Storage) C CoreFoundation
  23. 16 2304 144.0 CFDictionary (Key Storage) C CoreFoundation
  24. 8 512 64.0 CFBasicHash CFType CoreFoundation
  25. 7 560 80.0 CFArray ObjC CoreFoundation
  26. 6 768 128.0 CFPrefsPlistSource ObjC CoreFoundation
  27. 6 480 80.0 OS_os_log ObjC libsystem_trace.dylib
  28. 5 160 32.0 NSMergePolicy ObjC CoreData
  29. 4 384 96.0 NSLock ObjC Foundation
  30. ......
  31. -----------------------------------------------------------------------
  32. Zone Custom Object Zone_0x1004fe000: 1 nodes (32 bytes)
  33. COUNT BYTES AVG CLASS_NAME TYPE BINARY
  34. ===== ===== === ========== ==== ======
  35. 1 32 32.0 non-object
  36. -----------------------------------------------------------------------
  37. Zone GFXMallocZone_0x1004d8000: 0 nodes (0 bytes)

一共有3个zone,Zone Custom Object Zone_0x1004fe000: 1 nodes (32 bytes)就是我们创建的NSZone,不过它里面只有一个节点,共32bytes,如果你不设置Zone的name,它会是0bytes。所以我们可以推导出这32bytes是用来存储Zone本身的信息的。我们创建的1000个CustomObject其实在Zone DefaultMallocZone_0x1004c9000里,也就是系统默认创建的NSZone。如果你真的想用Zone内存机制,可以使用malloc_zone_t。通过下面的代码可以在自定义的zone上malloc内存块。

  1. void allocCustomObjectsWithCustomMallocZone() {
  2. malloc_zone_t *customZone = malloc_create_zone(1024, 0);
  3. malloc_set_zone_name(customZone, "custom malloc zone");
  4. for (int i = 0; i < 1000; ++i) {
  5. malloc_zone_malloc(customZone, 300 * 4096);
  6. }
  7. }

再次使用heap工具查看。我只截取了custom malloc zone的内容。有1001个node,也就是1000个malloc_zone_malloc出来的内存块加上zone本身的信息所占的内存块。

  1. -----------------------------------------------------------------------
  2. Zone custom malloc zone_0x1004fe000: 1001 nodes (1228800032 bytes)
  3. COUNT BYTES AVG CLASS_NAME TYPE BINARY
  4. ===== ===== === ========== ==== ======
  5. 1001 1228800032 1227572.4 non-object

我们可以使用malloc_destroy_zone(customZone)一次性释放上面分配的所有内存。

参考

iOS底层系统:虚拟内存
探索iOS内存分配
浅谈进程地址空间与虚拟存储空间
[ WWDC2018 ] - 深入解析iOS内存 iOS Memory Deep Dive
关于iOS内存的深入排查和优化

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注