iOS内存分配与五大区域

目录:

    • 参考的博客:
    • iOS内存的五大区域 :
      • 栈区(stack)
      • 堆区(heap):
      • 全局区(又称静态区)(static):
      • 文字常量区:
      • 程序代码区:
    • Allocations模版:
    • 虚拟内存简介:
    • 内存分页:
    • VM Region
    • 堆(heap)和 VM Region
    • VM Region Size:
    • malloc 和 calloc:
    • malloc_zone_t 和 NSZone:
    • 什么是VM Tracker:
    • VM Tracker列属性解析:
    • 使用vm_allocate自定义VM Region:
    • VM Region的Type:

参考的博客:

探索iOS内存分配
iOS内存深入探索之VM Tracker
使用 Instruments 检测内存泄漏

iOS内存的五大区域 :

首先我们先简单了解一下iOS中内存的五大区域,便于了解后续内容。

栈区(stack)

**由编译器管理(分配释放)**存放函数参数值、局部变量的值(函数中的基本数据类型)栈区的操作方式类似于数据结构中的栈(先进后出)。

堆区(heap):

由程序员管理(分配释放),若程序员不释放,程序结束时可能由系统(OS)回收,存放程序员new出来的对象。堆的操作方式于数据结构中的堆不同,操作方式类似于链表。

全局区(又称静态区)(static):

由编译器管理(分配释放),程序结束后由系统释放。存放全局变量和静态变量。有两块区域组成全局区(静态区),一块是存放未初始化的全局变量和静态变量,另一块是初始化完成的全局变量和静态变量,这两块区域是相邻的。

文字常量区:

由编译器管理(分配释放),程序结束后由系统释放。存放常量字符串。

程序代码区:

存放函数的二进制代码。

详细的五大分区优缺陷介绍等可参考该博客:[iOS]-内存的五大分区

(注: 到此为止iOS中的内存五大分区就介绍完啦,我们可以看到,这几大分区中只有堆区是由程序员管理的,所以我们后续主要探究的也就是堆区上的操作和原理)

下面我们就开始讲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位进程则更多,可达16EB。只有我们开始使用申请到的虚拟内存时,系统才会将虚拟地址映射到物理地址上,从而让程序使用真实的物理内存。下面是一个简易示意图:

iOS内存分配与五大区域_第1张图片
进程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进行分页。系统将内存页分为三种状态。

  1. 活跃内存页(active pages)- 这种内存页已经被映射到物理内存中,而且近期被访问过,处于活跃状态。
  2. 非活跃内存页(inactive pages)- 这种内存页已经被映射到物理内存中,但是近期没有被访问过。
  3. 可用的内存页(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。

iOS内存分配与五大区域_第2张图片
使用下面的代码:

int main(int argc, const char * argv[]) {
    NSMutableSet *objs = [NSMutableSet new];
    @autoreleasepool {
        for (int i = 0; i < 1000; ++i) {
            TestObject *obj = [TestObject new];
            [objs addObject:obj];
        }
        sleep(100000);
    }
    return 0;
}

TestObject是一个简单的OC类,只包含一个long类型的数组属性。

@interface TestObject : NSObject {
    long a[200];
}

@end

运行Profile,选择Allocation模版,进入后再添加VM Track模版,如果Allocation模版自带的VM Track不工作,就自己手动加一个。
iOS内存分配与五大区域_第3张图片
iOS内存分配与五大区域_第4张图片
iOS内存分配与五大区域_第5张图片
左上角这个红色嵌套的按钮是运行键,点击这个按钮就可以查看该项目中的内存情况。
请添加图片描述
可以看到这上图中最后一行的TestObject有1000个实例,点击TestObject右边的箭头,查看对象地址。
iOS内存分配与五大区域_第6张图片
我们发现第一个地址是0x126008200。我们接着切换到下面的VM Track,将模式调整为Regions Map。
iOS内存分配与五大区域_第7张图片
然后找到Address Range包含0x126008200的Region,如上图中最后一行即符合条件。我们发现这个 Region的Type是MALLOC_SMALL。点击箭头查看详情,你将会看到这个Region中的内存页列表。
iOS内存分配与五大区域_第8张图片
我们可以清晰地看到,从图中标蓝的那行开始到包含那一千个TestObject的实例地址结束的行的右边,内存页Swapped列下都是被标记的,因为我测试的是Mac上的App,所以当内存页不活跃时会被交换到硬盘上。这也就验证了我们在上面提到的交换机制。如果我们将TestObject的尺寸变大,比如作如下变动。

@interface TestObject : NSObject {
    long a[20000];
}

@end

内存上会有什么变化呢,答案是TestObject会被移动到MALLOC_MEDIUM内存区。
请添加图片描述
我们从上图可以看到,TestObject实例所占的内存是相当之大的
iOS内存分配与五大区域_第9张图片
我们找到这些实例的地址,然后接着去VM Track的Regions Map模式里去找TestObject实例所对应的页表。iOS内存分配与五大区域_第10张图片
我们看到TestObject实例的类型变为了MALLOC_MEDIUM

所以总的来说,堆区会被划分成很多不同的VM Region,不同类型的内存分配根据需求进入不同的VM Region。除了MALLOC_MEDIUM和MALLOC_SMALL外,还有MALLOC_TINY,MALLOC_LAEGE, MALLOC metadata等等。

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.

malloc 和 calloc:

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

void *memBlock = malloc(1024 * 1024);

iOS内存分配与五大区域_第11张图片
如上图中最后一行所示,MALLOC_TINY中有一块虚拟内存大小为1M的VM Region。因为我们没有使用这块内存,所以其他Size都是0。现在我们加上memset再观察。

void *memBlock = malloc(1024 * 1024);
memset(memBlock, 0, 1024 * 1024);

iOS内存分配与五大区域_第12张图片
从上图最后一行的信息中我们发现Resident Size,Dirty Size的值已经不是0了,说明这块内存已经被映射到物理内存中去了。为了解决这个问题,苹果官方推荐使用calloc代替malloc,calloc返回的内存区域会自动清零,而且只有使用时才会关联到物理内存并清零。

malloc_zone_t 和 NSZone:

相信大家对NSZone并不陌生,allocWithZone或者copyWithZone这2个方法大家应该也经常见到。那么Zone究竟是什么呢?Zone可以被理解为一组内存块,在某个Zone里分配的内存块,会随着这个Zone的销毁而销毁,所以Zone可以加速大量小内存块的集体销毁。不过NSZone实际上已经被苹果抛弃。你可以创建自己的NSZone,然后使用allocWithZone将你的OC对象在这个NSZone上分配,但是你的对象还是会被分配在默认的NSZone里。例如:

    static NSMutableSet *objs = nil;
    if (objs == nil) { objs = [NSMutableSet new]; }
    
    NSZone *testZone = NSCreateZone(1024, 1024, YES);
    NSSetZoneName(testZone, @"Test Object Zone");
    for (int i = 0; i < 1000; ++i) {
        TestObject *obj = [TestObject allocWithZone:testZone];
        [objs addObject:obj];
    }

代码创建了1000个TestObject对象,但是最后其实都在系统默认床架的NSZone中,Test Object Zone中只有1个node,其中是用来存放Zone本身的信息的,如果你真的想用Zone内存机制,可以使用malloc_zone_t。通过下面的代码可以在自定义的zone上malloc内存块,例如:

    malloc_zone_t *testZone = malloc_create_zone(1024, 0);
    malloc_set_zone_name(testZone, "Test malloc zone");
    for (int i = 0; i < 1000; ++i) {
        malloc_zone_malloc(testZone, 300 * 4096);
    }

最后运行的结果是我们的Test malloc zone中有1001个node,也就是1000个Test_zone_malloc出来的内存块加上zone本身的信息所占的内存块。

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

**总结:**本文主要介绍了iOS (OSX)系统中VM的相关原理,以及如何使用VM Track模板来分析VM Regions。

什么是VM Tracker:

VM Tracker是Xcode Instruments自带的一个内存分析工具,可以帮助你快速查看虚拟内存块的用量状态以及根据虚拟内存块的tag进行分类。如果你想知道关于虚拟内存的相关知识,可以先阅读上文,如果你对虚拟内存以及VM Region不太了解的话,阅读下面的内容可能会有些障碍。想要使用VM Tracker,使用Instruments的Allocations模版即可。如果模版自带的VM Tracker不显示信息,可以用右边的加号再添加一个VM Tracker。

VM Tracker列属性解析:

iOS内存分配与五大区域_第13张图片
上面是一个空的iOS App的VM Tracker示意图。一共有9列,下面我来一一解释它们的含义。

  • % of Res, 当前Type的VM Regions总Resident Size占比。
  • Type,VM Regions的Type,All和Dirty算是统计性质的Type,__TEXT表示代码段的内存映射,__DATA表示数据段的内存映射。MALLOC_TINY,MALLOC_LARGE,CG Image等Type可以从VM Region的Extend Info中读取出来,后面会着重介绍。
  • # Regs,当前Type的VM Region总数。
  • Path,VM Region是从哪个文件映射过来,因为有些类似于__DATA和mapped file的内存块是从文件直接映射过来的。
  • Resident Size,使用的物理内存量。
  • Dirty Size,使用中的物理内存块如果不交换到硬盘保存状态就不能复用,那么就是Dirty的内存块,比如你主动malloc出来的内存块,如果不保留其中的状态就把它给别人用,那你肯定就无法恢复这个内存块的信息,所以它是Dirty的。如果是一个映射到内存的文件,就算使用它的内存块,还是可以重新从磁盘载入文件到内存的,所以是非Dirty的,比如最上面图中的mapped file那一行,你可以看到Dirty Size是0。
  • Swapped Size, 在OSX中,不活跃的内存页可以被交换到硬盘,这是被交换的大小。在iOS中,只有非Dirty的内存页可以被交换,或者说是被卸载。
  • Virtual Size,VM Regions所占虚拟内存的大小
  • Res. %,Resident Size在Virtual Size中的占比

使用vm_allocate自定义VM Region:

我们可以使用vm_allocate方法申请一块虚拟内存。下面是具体代码。

	vm_address_t address;
	vm_size_t size = 1024 * 1024 * 100;
	vm_allocate((vm_map_t)mach_task_self(), &address, size, VM_MAKE_TAG(200) | VM_FLAGS_ANYWHERE);
	sleep(100000);

上面的代码申请了一块100M的虚拟内存,(vm_map_t)mach_task_self()表示在自己的进程空间内申请。size的单位是byte。 VM_MAKE_TAG(200)是给你申请的内存块提供一个Tag标记。我这里提供了一个200数值作为标记,后面我会具体介绍这个数值在VM Tracker中的作用。最后我们用VM Tracker看一下我们自己分配的虚拟内存块。

iOS内存分配与五大区域_第14张图片
从上图最后两行我们看到这块内存块的Resident Size和Dirty Size都是0KB,因为我们并未使用这块内存,所以并没有虚拟内存被关联到物理内存上去。你可以尝试使用这块内存,然后去VM Tracker观察变化。比如使用下面的方式填充内存块:

for (int i = 0; i < 1024 * 1024 * 100; ++i) {
  *((char *)address + i) = 0xab;
}

iOS内存分配与五大区域_第15张图片
我们看到这块内存块的Resident Size和Dirty Size都是64KB,大部分都被交换到了磁盘当中保存即都在Swapped中,然后过了一会儿之后全部都在Swapped中,Resident Size和Dirty Size都是0KB

VM Region的Type:

接下来我们来介绍内存块的Type,我曾经思考很久VM Tracker是如何识别出每个内存块的Type的。比如MALLOC_TINY,MALLOC_SMALL,ImageIO等等。答案就在vm_allocate方法的最后一个参数flags。flags可以分成2个部分。VM_FLAGS_ANYWHERE属于flags里控制内存分配方式的flag,它表示可以接受任意位置的内存分配。它的宏定义如下。

#define VM_FLAGS_ANYWHERE	0x0001

从定义可以看出,2个字节就可以存储它,int有4个字节,还剩下2个就可以用来存储标记内存类型的Type了。苹果提供了VM_MAKE_TAG宏帮助我们快速设置Type。VM_MAKE_TAG实际上做了一件很简单的事情,把值左移24个bit,也就是3个字节,所以系统留给了我们1个字节(只用一个字节表示tag)来表示内存的类型。下面是VM_MAKE_TAG的宏定义。

#define VM_MAKE_TAG(tag) ((tag) << 24)

实际上苹果已经内置了很多默认的Type,下面列出一部分。

#define VM_MEMORY_MALLOC 1
#define VM_MEMORY_MALLOC_SMALL 2
#define VM_MEMORY_MALLOC_LARGE 3
#define VM_MEMORY_MALLOC_HUGE 4
#define VM_MEMORY_SBRK 5// uninteresting -- no one should call
#define VM_MEMORY_REALLOC 6
#define VM_MEMORY_MALLOC_TINY 7
#define VM_MEMORY_MALLOC_LARGE_REUSABLE 8
#define VM_MEMORY_MALLOC_LARGE_REUSED 9

如果我们使用VM_MEMORY_MALLOC_HUGE来作为Type,再用VM Tracker观察会怎么样呢?下面是内存分配的代码。

vm_address_t address;
vm_size_t size = 1024 * 1024 * 100;
vm_allocate((vm_map_t)mach_task_self(), &address, size, VM_MAKE_TAG(VM_MEMORY_MALLOC_HUGE) | VM_FLAGS_ANYWHERE);

iOS内存分配与五大区域_第16张图片
如上图中的MALLOC_HUGE,很明显VM Tracker认出了这块内存,并且将它的Type设定为MALLOC_HUGE。如果你想使用vm_allocate来分配和管理大内存,也可以设置一个Type,方便快速定位到自己的虚拟内存块。

并且有一个细节: 在测试中发现,苹果对于正在使用和执行的这个VM Region,它的Resident Size和Dirty Size都是它的完整的Virtual Size(虚拟内存的大小),等执行完成之后就会很快进行磁盘和内存的交换,最后所有的内存都写到了磁盘中,即Resident Size和Dirty Size都是0KB,Swapped都是完整的Virtual Size(虚拟内存的大小)。

总结: 本文主要介绍了VM Tracker中关于虚拟内存的一些概念,以及如何自行分配虚拟内存。了解了这些之后,在分析内存暴涨或者泄漏时就有了新的思路,而不仅仅是局限于基于malloc内存块的内存分析了。

你可能感兴趣的:(ios,objective-c,开发语言)