iOS内存分配与五大区域

参考的博客:

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

在这里插入图片描述
进程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。

请添加图片描述
使用下面的代码:

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

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

@end

内存上会有什么变化呢,答案是TestObject会被移动到MALLOC_MEDIUM内存区。
请添加图片描述
我们从上图可以看到,TestObject实例所占的内存是相当之大的
请添加图片描述
我们找到这些实例的地址,然后接着去VM Track的Regions Map模式里去找TestObject实例所对应的页表。请添加图片描述
我们看到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);

请添加图片描述
如上图中最后一行所示,MALLOC_TINY中有一块虚拟内存大小为1M的VM Region。因为我们没有使用这块内存,所以其他Size都是0。现在我们加上memset再观察。

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

请添加图片描述
从上图最后一行的信息中我们发现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 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看一下我们自己分配的虚拟内存块。

请添加图片描述
从上图最后两行我们看到这块内存块的Resident Size和Dirty Size都是0KB,因为我们并未使用这块内存,所以并没有虚拟内存被关联到物理内存上去。你可以尝试使用这块内存,然后去VM Tracker观察变化。比如使用下面的方式填充内存块:

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

请添加图片描述
我们看到这块内存块的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);

请添加图片描述
如上图中的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内存块的内存分析了。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: iOS内存管理版本记录如下: 1. iOS 2.0及更早版本:使用手动管理内存的方式。 2. iOS 3.0:引入了基于引用计数的自动内存管理,使用retain和release函数来增加或减少对象的引用计数。 3. iOS 5.0:引入了ARC(自动引用计数)机制,ARC会在编译时自动插入retain和release代码,减少手动管理内存的工作。 4. iOS 7.0:引入了内存诊断工具Memory Usage Report,可以监测App内存使用情况,帮助开发者优化内存管理。 5. iOS 8.0:引入了一些新的API,如NSCache和NSURLSession,使得内存管理更加方便和灵活。 6. iOS 11.0:引入了基于图片大小的UIImage渲染机制,减少了内存占用。 7. iOS 13.0:引入了叫做“Scene”的多任务环境,使得内存管理更加复杂,需要更加小心谨慎地处理内存问题。 总的来说,随着iOS版本的不断更新,内存管理的机制也在不断地完善和优化,使得iOS应用能够更加高效地使用内存,提高用户体验。 ### 回答2: iOS的内存管理是由操作系统自动管理的,在不同的版本中有所不同。 在iOS 5之前的版本中,内存管理主要依赖于手动管理引用计数(reference counting)来管理对象的生命周期。开发者需要手动调用retain和release方法来增加或减少对象的引用计数,以确保对象在不再需要时能够被正确释放。这种方式需要开发者非常谨慎地管理对象的引用,以避免内存泄漏或野指针等问题。 从iOS 5开始,iOS引入了自动引用计数(Automatic Reference Counting,ARC)的内存管理机制。ARC可以自动地插入retain、release和autorelease等方法的调用,使得开发者不再需要手动进行内存管理。开发者只需要关注对象的创建和使用,而不需要关心具体的内存管理细节。ARC减少了内存管理的工作量,提高了开发效率,并且减少了内存泄漏和野指针等问题的发生。不过,ARC并不是完全的自动化内存管理,开发者仍然需要遵循一些规则,比如避免循环引用等,以保证内存的正确释放。 随着iOS版本的不断更新,苹果不断改进和优化内存管理机制。每个新版本都带来了更好的性能和更高效的内存管理。开发者可以通过关注苹果的官方文档和开发者社区中的更新内容来了解每个版本中的具体变化和改进。 总结来说,iOS的内存管理从手动的引用计数到自动引用计数的演变,极大地简化了开发者的工作,并提高了应用的性能和稳定性。随着不断的改进和优化,iOS的内存管理会越来越高效和可靠。 ### 回答3: iOS内存管理版本记录是指苹果公司在不同版本的iOS操作系统中对于内存管理方面的改进和更新记录。随着iOS版本的不断迭代,苹果在内存管理方面进行了一系列的优化和改进,以提高系统的稳定性和性能。 首先,在早期的iOS版本中,苹果采用了手动内存管理的方式,即开发人员需要手动创建和释放内存,容易出现内存泄漏和内存溢出等问题。为了解决这些问题,苹果在iOS5版本中引入了自动引用计数(ARC)机制。ARC机制能够通过编译器自动生成内存管理代码,避免了手动管理内存带来的问题。 其次,iOS6版本引入了内存分页机制。这个机制能够将应用程序内存分成不同的页,将不常用的页置于闲置列表中,从而释放出更多的内存空间。这些闲置列表中的页能够在需要时快速恢复到内存中,减少了内存压力。 此外,iOS7版本中进一步提升了内存管理的能力。苹果在这个版本中引入了内存压缩技术,将内存中的数据进行压缩,从而提高了内存利用率。此外,iOS7还引入了资源清理功能,可以自动清理不再使用的资源,释放内存空间。 最后,在iOS13版本中,苹果进一步改进了内存管理策略。该版本中引入了后台内存优化功能,能够自动优化应用在后台运行时的内存占用,减少了后台应用对于系统内存的占用和影响。 综上所述,iOS内存管理版本记录反映了苹果在不同版本的iOS操作系统中对于内存管理方面的改进和优化。这些改进和优化使得iOS系统更加稳定和高效,并且提升了应用程序的性能和用户体验。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值