前言
在运行 iOS(OSX)程序时,左侧的 Debug Navigator 中可以看见当前使用的内存.我们也可以使用 Instruments 的 Allocations 模板来追踪对象的创建和释放.不知道你是否也曾困惑于 Debug Navigator 显示的内存和 Allocations 显示的总内存对不上号的问题.本篇文章将带你深入了解 iOS 的内存分配.
Allocations 模版
在 Instruments 的 Allocations 模板中,可以看到主要统计的是
All Heap & Anonymous VM
的内存使用量.All Heap 好理解,就是 App 运行过程中在堆上分配的内存.我们可以通过搜索关键字查看你关注的类在堆上的内存分配情况.那么 Anonymous VM 是什么呢?按照官方描述,它是和你的 App 进程关联比较大的 VM regions.原文如下.
interesting VM regions such as graphics- and Core Data-related. Hides mapped files, dylibs, and some large reserved VM regions.
虚拟内存简介
什么是 VM Regions 呢?要知道这个首先要了解什么是虚拟内存.当我们向系统申请内存时,系统并不会给你返回物理内存的地址,而是给你一个虚拟内存地址.每个进程都拥有相同大小的虚拟地址空间,对于 32 位的进程,可以拥有 4GB 的虚拟内存,64 位进程则更多,可达 18EB.只有我们开始使用申请到的虚拟内存时,系统才会将虚拟地址映射到物理地址上,从而让程序使用真实的物理内存.下面是一个示意图,我简化了概念.
进程 A 和 B 都拥有 1 到 4 的虚拟内存.系统通过虚拟内存到物理内存的映射,让 A 和 B 都可以使用到物理内存.上图中物理内存是充足的,但是如果 A 占用了大部分内存,B 想要使用物理内存的时候物理内存却不够该怎么办呢?在 OSX 上系统会将不活跃的内存块写入硬盘,一般称之为 swapping out.iOS 上则会通知 App,让 App 清理内存,也就是我们熟知的 Memory Warning.
内存分页
系统会对虚拟内存和物理内存进行分页,虚拟内存到物理内存的映射都是以页为最小粒度的.在 OSX 和早期的 iOS 系统中,物理和虚拟内存都按照 4KB 的大小进行分页.iOS 近期的系统中,基于 A7 和 A8 处理器的系统,物理内存按照 4KB 分页,虚拟内存按照 16KB 分页.基于 A9 处理器的系统,物理和虚拟内存都是以 16KB 进行分页.系统将内存页分为三种状态.
活跃内存页(active pages)- 这种内存页已经被映射到物理内存中,而且近期被访问过,处于活跃状态.
非活跃内存页(inactive pages)- 这种内存页已经被映射到物理内存中,但是近期没有被访问过.
可用的内存页(free pages)- 没有关联到虚拟内存页的物理内存页集合.
当可用的内存页降低到一定的阀值时,系统就会采取低内存应对措施,在 OSX 中,系统会将非活跃内存页交换到硬盘上,而在 iOS 中,则会触发 Memory Warning,如果你的 App 没有处理低内存警告并且还在后台占用太多内存,则有可能被杀掉.
VM Region
为了更好的管理内存页,系统将一组连续的内存页关联到一个 VMObject 上,VMObject 主要包含下面的属性.
Resident pages - 已经被映射到物理内存的虚拟内存页列表
Size - 所有内存页所占区域的大小
Pager - 用来处理内存页在硬盘和物理内存中交换问题
Attributes - 这块内存区域的属性,比如读写的权限控制
Shadow - 用作(copy-on-write)写时拷贝的优化
Copy - 用作(copy-on-write)写时拷贝的优化 我们在 Instruments 的 Anonymous VM 里看到的每条记录都是一个 VMObject 或者也可以称之为 VM Region.
堆(heap)和 VM Region
那么堆和 VM Region 是什么关系呢?按照前面的说法,应该任何内存分配都逃不过虚拟内存这套流程,堆应该也是一个 VM Region 才对.我们应该怎样才能知道堆和 VM Region 的关系呢?Instruments 中有一个 VM Track 模版,可以帮助我们清楚的了解他们的关系.我创建了一个空的 Command Line Tool App.
使用下面的代码.
int main(int argc, const char * argv[]) {
NSMutableSet *objs = [NSMutableSet new];
@autoreleasepool {
for (int i = 0; i < 1000; ++i) {
CustomObject *obj = [CustomObject new];
[objs addObject:obj];
}
sleep(100000);
}
return 0;
}
CustomObject 是一个简单的 OC 类,只包含一个 long 类型的数组属性.
@interface CustomObject() {
long a[200];
}
@end
运行 Profile,选择 Allocation 模版,进入后再添加 VM Track 模版,这里不知道为什么 Allocation 模版自带的 VM Track 不工作,只能自己手动加一个了.
我们在
All Heap & Anonymous VM
下可以看到,CustomObject 有 1000 个实例,点击 CustomObject 右边的箭头,查看对象地址.
第一个地址是
0x7faab2800000
.我们切换到最底下的 VM Track,将模式调整为 Regions Map.
然后找到 Address Range 为
0x7faab2800000
开头的 Region,我们发现这个 Region 的 Type 是 MALLOC_SMALL.点击箭头看详情,你将会看到这个 Region 中的内存页列表.
可能你已经发现了,截图中的内存页 Swapped 列下都是被标记的,因为我测试的是 Mac 上的 App,所以当内存页不活跃时会被交换到硬盘上.这也就验证了我们在上面提到的交换机制.如果我们将 CustomObject 的尺寸变大,比如作如下变动.
@interface CustomObject() {
long a[20000];
}
@end
内存上会有什么变化呢?答案是 CustomObject 会被移动到 MALLOC_LARGE 内存区.
所以总的来说,堆区会被划分成很多不同的 VM Region,不同类型的内存分配根据需求进入不同的 VM Region.除了 MALLOC_LARGE 和 MALLOC_SMALL 外,还有 MALLOC_TINY, MALLOC metadata 等等.具体什么样的内存分配进什么样的 VM Region,我自己也还在探索中.
VM Region Size
我们在 VM Track 中可以看到,一个 VM Region 有 4 种 size.
Dirty Size
Swapped Size
Resident Size
Virtual Size Virtual Size 顾名思义,就是虚拟内存大小,将一个 VM Region 的结束地址减去起始地址就是这个值.Resident Size 指的是实际使用物理内存的大小.Swapped Size 则是交换到硬盘上的大小,仅 OSX 可用.Dirty Size 根据官方的解释我的理解是如果一个内存页想要被复用,必须将内容写到硬盘上的话,这个内存页就是 Dirty 的.下面是官方对 Dirty Size 的解释.secondary storage 可以理解为硬盘.
The amount of memory currently being used that must be written to secondary storage before being reused.
所以一般来说 app 运行过程中在堆上动态分配的内存页都是 Dirty 的,加载动态库或者文件内存映射产生的内存页则是非 Dirty 的.综上,我们可以总结出,
Virtual Size >= Resident Size + Swapped Size >= Dirty Size + Swapped Size
,
malloc 和 calloc
我们除了使用 NSObject 的 alloc 分配内存外,还可以使用 c 的函数 malloc 进行内存分配.malloc 的内存分配当然也是先分配虚拟内存,然后使用的时候再映射到物理内存,不过 malloc 有一个缺陷,必须配合 memset 将内存区中所有的值设置为 0.这样就导致了一个问题,malloc 出一块内存区域时,系统并没有分配物理内存.然而,调用 memset 后,系统将会把 malloc 出的所有虚拟内存关联到物理内存上,因为你访问了所有内存区域.我们通过代码来验证一下.在 main 方法中,创建一个 1024*1024 的内存块,也就是 1M.
void *memBlock = malloc(1024 * 1024);
我们发现 MALLOC_LARGE 中有一块虚拟内存大小为 1M 的 VM Region.因为我们没有使用这块内存,所以其他 Size 都是 0.现在我们加上 memset 再观察.
void *memBlock = malloc(1024 * 1024);
memset(memBlock, 0, 1024 * 1024);
现在 Resident Size,Dirty Size 也是 1M 了,说明这块内存已经被映射到物理内存中去了.为了解决这个问题,苹果官方推荐使用 calloc 代替 malloc,calloc 返回的内存区域会自动清零,而且只有使用时才会关联到物理内存并清零.
malloc_zone_t 和 NSZone
相信大家对 NSZone 并不陌生,allocWithZone 或者 copyWithZone 这 2 个方法大家应该也经常见到.那么 Zone 究竟是什么呢?Zone 可以被理解为一组内存块,在某个 Zone 里分配的内存块,会随着这个 Zone 的销毁而销毁,所以 Zone 可以加速大量小内存块的集体销毁.不过 NSZone 实际上已经被苹果抛弃.你可以创建自己的 NSZone,然后使用 allocWithZone 将你的 OC 对象在这个 NSZone 上分配,但是你的对象还是会被分配在默认的 NSZone 里.我们可以用 heap 工具查看进程的 Zone 分布情况.首先使用下面的代码让 CustomObject 使用新的 NSZone.
void allocCustomObjectsWithCustomNSZone() {
static NSMutableSet *objs = nil;
if (objs == nil) { objs = [NSMutableSet new]; }
NSZone *customZone = NSCreateZone(1024, 1024, YES);
NSSetZoneName(customZone, @"Custom Object Zone");
for (int i = 0; i < 1000; ++i) {
CustomObject *obj = [CustomObject allocWithZone:customZone];
[objs addObject:obj];
}
}
代码创建了 1000 个 CustomObject 对象,并且尝试使用新建的 Zone.我们用 heap 工具看看结果.首先使用 Activity Monitor 找到进程的 PID,在命令行中执行
heap PID
执行的结果大致如下.
......
Process 25073: 3 zones
Zone DefaultMallocZone_0x1004c9000: Overall size: 196992KB; 13993 nodes malloced for 160779KB (81% of capacity); largest unused: [0x102800000-171072KB]
Zone Custom Object Zone_0x1004fe000: Overall size: 1024KB; 1 nodes malloced for 1KB (0% of capacity); largest unused: [0x102200000-1024KB]
Zone GFXMallocZone_0x1004d8000: Overall size: 0KB
All zones: 13994 nodes malloced - 160779KB
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]
Zone Custom Object Zone_0x1004fe000: 1 nodes - Sizes: 32[1]
Zone GFXMallocZone_0x1004d8000: 0 nodes
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]
Found 523 ObjC classes
Found 56 CFTypes
-----------------------------------------------------------------------
Zone DefaultMallocZone_0x1004c9000: 13993 nodes (164637440 bytes)
COUNT BYTES AVG CLASS_NAME TYPE BINARY
===== ===== === ========== ==== ======
12771 779136 61.0 non-object
1000 163840000 163840.0 CustomObject ObjC VMResearch
49 2864 58.4 CFString ObjC CoreFoundation
21 1344 64.0 pthread_mutex_t C libpthread.dylib
20 1280 64.0 CFDictionary ObjC CoreFoundation
18 2368 131.6 CFDictionary (Value Storage) C CoreFoundation
16 2304 144.0 CFDictionary (Key Storage) C CoreFoundation
8 512 64.0 CFBasicHash CFType CoreFoundation
7 560 80.0 CFArray ObjC CoreFoundation
6 768 128.0 CFPrefsPlistSource ObjC CoreFoundation
6 480 80.0 OS_os_log ObjC libsystem_trace.dylib
5 160 32.0 NSMergePolicy ObjC CoreData
4 384 96.0 NSLock ObjC Foundation
......
-----------------------------------------------------------------------
Zone Custom Object Zone_0x1004fe000: 1 nodes (32 bytes)
COUNT BYTES AVG CLASS_NAME TYPE BINARY
===== ===== === ========== ==== ======
1 32 32.0 non-object
-----------------------------------------------------------------------
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 内存块.
void allocCustomObjectsWithCustomMallocZone() {
malloc_zone_t *customZone = malloc_create_zone(1024, 0);
malloc_set_zone_name(customZone, "custom malloc zone");
for (int i = 0; i < 1000; ++i) {
malloc_zone_malloc(customZone, 300 * 4096);
}
}
再次使用 heap 工具查看.我只截取了 custom malloc zone 的内容.有 1001 个 node,也就是 1000 个 malloc_zone_malloc 出来的内存块加上 zone 本身的信息所占的内存块.
-----------------------------------------------------------------------
Zone custom malloc zone_0x1004fe000: 1001 nodes (1228800032 bytes)
COUNT BYTES AVG CLASS_NAME TYPE BINARY
===== ===== === ========== ==== ======
1001 1228800032 1227572.4 non-object
我们可以使用
malloc_destroy_zone(customZone)
一次性释放上面分配的所有内存.
总结
本文主要介绍了 iOS (OSX)系统中 VM 的相关原理,以及如何使用 VM Track 模板来分析 VM Regions,本文只是关注了 MALLOC 相关的几个 VM Region,还有其他专用的一些 VM Region,通过研究他们的内存分配,可以有针对性的对内存进行优化,这就是接下来要做的事情.
来源: https://juejin.im/post/5a5e13c45188257327399e19