├── 前言.md ├── 申请分配内存贴示.md ├── 缓存和可清除内存.md └── 虚拟内存系统.md /前言.md: -------------------------------------------------------------------------------- 1 | #简介# 2 | 内存是所有程序在运行时重要的系统资源。程序在运行之前必须先将其加载入内存当中,在程序运行期间,他们会显式或者隐式地申请额外的内存去存储或者处理他们的(程序级别)数据。为程序的代码和数据开辟内存空间需要耗费一定时间和系统资源,因此对系统的全局性能有影响。当然,尽管你不可能避免不使用内存,但是仍然有很多方法能够帮助你减少因为内存使用而对系统性能带来的影响。 3 | 4 | 本文主要讲述了**OS X**以及**IOS**的内存系统的背景知识,以及应该如何高效的使用上述两种平台下的内存。你可以利用本文所讲述的这些知识去优化你的程序中内存的使用,从而确保你在正确的时间申请了正确数量的内存。本文同样也会告诉你一些小贴示来帮助查探你的程序中因内存是否合理使用带来的性能影响问题。 5 | 6 | 本文的组织结构如下: 7 | 8 | * [关于虚拟内存的相关知识](1) : 本文会介绍相关的术语并会从宏观角度介绍关于**OS X**和**IOS**的的虚拟内存的相关知识。 9 | * [关于申请内存的一些小贴士](2) : 本文会介绍关于内存申请,内存初始化以及内存拷贝的最好的技术,同样也会针对**IOS**中低内存警告环境下提出一些合适的应对措施。 10 | * [缓存以及释放内存](3) : 本文主要讲述了缓存能为程序带来哪些收益以及如何避免因为使用缓存导致的一些问题。同样也详细讲述了在如何缓存系统中成功使用这种能为程序带来收益的**释放内存**技术。 11 | * [追踪内存使用情况](4) : 本文主要讲述了能够帮助你分析程序中内存使用情况的工具以及相关技术。 12 | * [查找内存泄漏](5) : 本文主要讲述了能够帮助你查找程序中内存泄漏的工具及相关技术。 13 | * [开启内存申请编译模式](6) : 本文主要讲述了使用环境变量开启**内存申请记录模式**。在使用一些内存分析工具前,你必须设置这些环境变量中的某几个。 14 | * [观测虚拟内存使用情况](7) : 本文主要描述了分析你的程序内存占用情况的工具盒相关技术。 -------------------------------------------------------------------------------- /申请分配内存贴示.md: -------------------------------------------------------------------------------- 1 | #分配内存贴示# 2 | 3 | 内存是你的应用中重要的资源,所以仔细思考下你的应用程序如何使用内存以及如何为你的应用程序高效地分配内存是十分重要的。大多数应用程序在进行分配内存操作时不需要做什么特殊的操作,只需要按需申请对象或者内存块即可,你也不会发现任何因分配内存而导致的应用程序的性能下降。但是对于使用大量内存资源的程序而言,仔细的规划你的内存申请分配策略可能会使你的应用程序大不同。 4 | 5 | 下面几节将讲述几条有关分配内存的基本操作的小贴示,如果照做的话,将会使得你的应用程序更加高效。首先为了判断你的应用程序是否存在因内存分配导致的性能问题,你需要使用Xcode的相关工具去查看你的运行状态下的应用程序内存分配情况。想知道如何做,请参看《[追踪你的内存使用情况](1)》。 6 | 7 | ##提升内存相关的性能的贴示## 8 | 9 | 当你设计你的代码的时候,你应该清楚的知道如何使用内存。因为内存是如此重要的资源,你要确定高效的使用内存资源且不要造成浪费。此外,针对指定的操作申请合适的内存大小也同样重要。下面的几节将会讲述几种提高你内存使用效率的方法。 10 | 11 | ###延迟你的内存分配时机### 12 | 13 | 每一个内存分配操作都会造成性能上的损耗。这种损耗包括了在你的程序的逻辑地址空间中分配内存空间耗费的时间,也包括了将逻辑地址空间映射至物理内存时耗费的时间。如果你不打算立刻使用某一片内存空间,那么如下操作将是一种十分明智的做法,即延迟该内存空间的分配时机直到你真正需要它时。例如,为了优化你的app的加载速度,你应该尽可能减少在加载应用时候申请内存的数量,于此对应的,你应该关注你的用户界面展示及用户交互时所需的内存的分配情况。延迟其它的内存分配申请直到你的用户真正开始与你的app进行交互。这种**懒分配内存策略(lazy allocation of memory)**会立竿见影的起到节省时间的效果,同时能够保证在真正需要用内存空间的时机分配合适的内存空间。 14 | 15 | 有一个地方使用**懒分配内存策略**需要点小技巧,即针对全局变量的内存申请。因为全局变量对于你的app而言是全局的。因此你需要确保你的代码在使用它前已经被正确分配内存空间且初始化。最基本的解决方法就是在你的函数模块里定义一个静态全局变量,然后用公共访问函数对该全局变量进行取值和赋值。如下代码: 16 | 17 | MyGlobalInfo* GetGlobalBuffer() 18 | { 19 | static MyGlobalInfo* sGlobalBuffer = NULL; 20 | if ( sGlobalBuffer == NULL ) 21 | { 22 | sGlobalBuffer = malloc( sizeof( MyGlobalInfo ) ); 23 | } 24 | return sGlobalBuffer; 25 | } 26 | 27 | 这段代码唯一需要你关心的就是当多线程调用该函数时可能会发生的状况。在多线程环境下,你需要使用**锁(Lock)**来保护 **if语句** ,否则可能出现**多线程竞争**。当然这个方法带来的负面影响就是当每个线程访问该全局变量时都要受到锁机制的限制,这会给你的应用带来极大的性能消耗。一个简单的解决上述性能下降问题的办法就是在你的其它子线程还没有访问该全局变量前,就在主线程上初始化该全局变量。 28 | 29 | ###高效初始化内存块### 30 | 31 | 使用**malloc**函数分配的小的内存块不能保证其对应的物理内存空间初始化为0,尽管你可以使用**memeset**函数来初始化上述的物理内存空间使之为0,但是更好的选择是使用**calloc**函数。**calloc**函数会保留所分配内存的虚拟内存地址,但是会直到真正使用该内存时才初始化它(这里笔者认为是真正使用时才会将虚拟内存与物理内存建立映射并将对应的物理内存块初始化为0)。这个方法比使用memeset高效很多,因为若memeset执行初始化为0的任务,需要迫使虚拟内存系统先将malloc申请的虚拟内存空间与实际物理地址空间建立映射关系。而使用calloc函数的好处就是只有当系统真正使用该内存时才会建立映射并初始化它们,不像malloc + memeset组合需要立刻将所有分配的内存建立映射并初始化。 32 | 33 | ###重用临时内存缓冲池### 34 | 35 | 如果你有一个自创大型临时内存缓冲池以用于计算操作的函数的话,你也许会考虑如何重用这些临时缓冲池而不是每次调用该函数时都要重新创建一遍。即使你的函数需要一个可变的缓冲池空间,你也可以当需要增加该缓冲池空间时使用 *realloc* 函数来进行操作。对于多线程环境下的应用,重用缓冲池最好的方法就是将它们加入你的线程所对应的**TLS(线程局部存储 thread-local-storage)**。当然,你也可以使用静态变量存储你的缓冲池(笔者注:这样就分配在堆的静态变量存储区域了),但是这么做就无法使得多线程同时访问了。使用缓冲池所带来的收益可以抵消频繁申请销毁大型内存块所带来的开销,但是这个方法只适用于那些频繁申请销毁内存块的情况。同时,你也要注意不要缓存太多内存,因为这样会增加你的应用的内存占用量,使用时要谨慎,仅当你测试后发现这样做确实能够提升你的应用性能时才这样做。 36 | 37 | ###释放无用内存### 38 | 39 | 对于使用malloc库申请分配的内存,最重要的一点就是当你用完以后一定要及时释放掉它。如果你忘记释放该内存,那么将会导致**内存泄漏(memory leaks)**,这样会减少你的应用所能够使用的内存,同时还会影响你的系统的性能。内存泄漏同时也可能导致你的应用因申请不到足够的内存而挂掉。 40 | >注意:使用ARC机制编译的应用app不要显式的释放OC对象。相对应的,如果你的app需要保留一个对象存在你的内存中,你需要使用**强引用(strong references)**,如果你不想让它保留该对象在内存中,则删除掉所有指向该对象的强引用即可,因为一旦一个对象没有任何强引用指向它,依据ARC机制,编译器会自动释放掉它。想了解更多有关ARC机制的信息,请阅读《*Transitioning to ARC Release Notes*》。 41 | 42 | 无论你在什么平台下书写你的应用,你都应该消除你的应用的内存泄漏。对于那些使用malloc申请分配内存的代码而言,请记住尽可能推迟申请内存的时机总是好的,但是可千万别推迟释放该内存的时机。如果你想追踪你的应用中的内存泄漏,请使用Instruments app。 43 | 44 | 45 | ##内存分配技术## 46 | 47 | 内存是一个如此基础且重要的资源,**OS X**和**IOS**都提供了好几种方式去分配内存。你使用的分配内存的方式主要取决于你的需求。但是归根结底,所有内存分配方法最终都会使用malloc库,甚至连Cocoa对象的内存分配最终都仍然使用的是malloc库。这样的单一库(malloc)申请分配内存的使用方式为我们使用性能测试工具检测应用内存申请分配的情况提供了可能。 48 | 49 | 如果你在编写Cocoa应用程序,你也许只会使用NSObject类下的alloc函数分配内存。即便如此,你也会在一些时候需要使用其它内存分配技术来申请内存。例如,你也许想要直接使用malloc函数来申请分配内存以避免调用一些低等级的函数(笔者注:大意就是说不用通过alloc函数一层层调用相关函数,最终调用到malloc函数了,直接用malloc申请内存)。 50 | 51 | 下面几节将会讲述有关malloc库和虚拟内存系统以及它们如何申请分配内存的相关信息。这些章节会帮助你理解每一种内存申请分配方式的耗费代价。你可以通过理解这些信息帮助优化你的应用程序的内存申请分配方式从而达到优化应用程序性能的目的。 52 | >注意:下面这些章节内容是假设你使用原版malloc库的原装函数申请分配你的内存,如果你使用的是自定义版本的malloc库的相关函数,这些技术可能对你无效。 53 | 54 | ###申请对象内存## 55 | 56 | 对于基于Objective-C的应用而言,你申请分配内存主要采用如下两种技术中的某一种,一是**alloc**方式,通过执行OC类的初始化函数进行初始化操作。另一种是使用**new**方式申请分配内存,通过调用默认的init函数对OC类进行初始化。 57 | 58 | 在创建一个OC类对象之后,编译器的ARC特性就会决定这个对象的生命周期以及什么时候它该被销毁。每一个新的OC类对象都至少需要一个强引用指针指向它以防止被编译器立刻销毁。因此,当你创建一个新的对象时,你总是需要创建至少一个强引用指针指向它。在此之后,你也许会创建额外的强引用指针或者弱引用指针指向该对象,当然,这些都是依据你的代码需求所决定的。当指向该对象的所有强引用指针被删除,那么编译器就会自动删除掉该对象。 59 | 60 | 想了解更多有关ARC的信息,以及如果管理你的对象的生命周期,请看《*Transitioning to ARC Release Notes*》一文。 61 | 62 | 63 | ###使用Malloc申请小内存块### 64 | 65 | 对于小内存块申请分配而言,这里的小是指比几页虚拟内存页还小,**malloc**函数会从一个能够逐步增长的空闲块链表中再分配所需要的内存数量。当你使用**free**函数释放这些小内存块后,这些内存块又会以一个合适的方式添加回这个空闲块链表中来。这个空闲块链表是由系统通过**vm_allocate**函数为你创建了几个虚拟内存页从而共同组成的。 66 | 67 | 当分配任何小内存块时,请记住malloc函数能够申请的最小空间为16B,也就是说,你最小只能够申请16B的内存块,或者申请16B的倍数级别的内存块。例如,你使用malloc函数申请4B的内存块,该函数将会返回一个16B的内存块,如果你申请的是24B的内存块,该函数返回32B的内存块。因为这样一个特性,你应该仔细的设计你的数据结构,尽可能使得它们的大小是16B的倍数。 68 | 69 | >因为这样一个特性,如果申请比一个虚拟内存页还小的内存块会导致内存无法**页对齐(page aligned)** 70 | 71 | ###使用Malloc申请大内存块### 72 | 73 | 对于大内存块申请分配而言,这里的大是指比几页虚拟内存页还大,**malloc**函数会自动使用**vm_allocate**函数获取所要求分配的内存。**vm_allocate**函数会为新的大内存块分配一个当前进程逻辑地址空间上的地址段,但是它不会立刻为这些虚拟地址空间建立任何与物理内存空间的映射关系。相对应的,内核会做如下操作: 74 | 75 | 1. 内核会先创建 *map entry* 将这些虚拟内存地址空间中的地址段匹配(map)进来,这个*map entry* 实际上就是一个定义了内存段起始地址与结束地址的简单的数据结构。 76 | 2. 这个内存段(range of memory)会被默认分页器(default pager)备份起来。 77 | 3. 内核会创建并初始化一个VM对象,并将该VM对象与上述*map entry*关联起来。 78 | 79 | 此时此刻,物理内存中没有任何页驻留,硬盘中的备份存储区域也没有任何被交换出来的页存在。所有的页都被系统虚拟映射着。当你的代码访问了内存块中的某一部分,例如读或者写了某一个特殊的地址,那么此时就会产生页错误,因为此时访问的虚拟地址并没有真正映射到任何一个物理内存页中去。在**OS X**平台下,内核在此时发现了存在没有在备份存储空间有映射关系的VM对象,然后就会针对每一个页错误做出如下操作: 80 | 81 | 1. 内核首先会从空闲(内存)链表中取出一个物理页并将其数据初始化为0。 82 | 2. 在VM对象的驻留内存页链表中插入一个指向步骤1分配出的页的引用对象。 83 | 3. 通过填充*pmap*这个数据结构的相关字段,完成虚拟内存页与物理内存页的映射关系。*pmap*结构中包含着记录着给定虚拟内存到实际物理内存映射关系的**页表(page table)**。 84 | 85 | 这种大内存块的粒度大小等同于虚拟内存页的大小,即4k。换句话说,任何大内存块的分配空间都是4k的倍数,如果申请分配的空间不是4k的倍数,那么也将会自动申请到4k的倍数。因此,如果你申请大内存块缓存,你应该申请4k倍数的大小以避免浪费内存。 86 | >注意:大内存块的申请分配可以保证内存页对齐(page-aligned) 87 | 88 | 对于大的内存块申请分配,你也许会发现直接使用**vm_allocate**申请虚拟内存地址会更直截了当且高效。下面的例子就是告诉你如何使用**vm_allocate**函数申请内存: 89 | 90 | void* AllocateVirtualMemory(size_t size) 91 | { 92 | char* data; 93 | kern_return_t err; 94 | // In debug builds, check that we have 95 | // correct VM page alignment 96 | check(size != 0); 97 | check((size % 4096) == 0); 98 | // Allocate directly from VM 99 | err = vm_allocate( (vm_map_t) mach_task_self(), 100 | (vm_address_t*) &data, 101 | size, 102 | VM_FLAGS_ANYWHERE); 103 | // Check errors 104 | check(err == KERN_SUCCESS); 105 | if(err != KERN_SUCCESS) 106 | { 107 | data = NULL; 108 | } 109 | return data; 110 | } 111 | 112 | 113 | ###批量分配内存### 114 | 115 | 如果你的代码想申请分配大量大小一致的内存块,你可以使用 **malloc_zone_batch_malloc** 函数来一次性申请分配完毕。同样申请分配大量大小一致内存块,该函数比数次调用malloc函数拥有更优秀的性能表现。尤其当每一个单独内存块的大小小于4K时,其性能达到最佳。该函数会尽最大可能返回你所要求的大小一致的内存块数量,但是请注意它也可能不会返回给你所需的数量。所以当使用该函数时,请一定仔细检查返回的内存块数量是否符合你的预期。**OS X** 10.3及其以上版本,IOS版本都支持批量分配内存。想知道更多,请查看 **/usr/include/malloc/malloc.h** 头文件。 116 | 117 | ###申请共享内存### 118 | 共享内存是一种可以被两个或者多个进程共同读写的内存。共享内存的使用途径包括以下几种情况: 119 | 120 | * 共享大的资源,例如图标或者声音等 121 | * 两个或者多个进程之间的快速沟通 122 | 123 | 共享内存很脆弱且通常在有其它可用办法的情况下不建议使用。如果一个程序破坏了共享内存的某一段,那么其它程序可能会访问到被破坏的共享内存。详细信息请查看 **/usr/include/sys/shm.h** 头文件。 124 | 125 | ###使用内存分配域(Malloc Memory Zones)### 126 | 127 | 所有的内存块都是在一个**malloc zone**(也有称为**malloc heap**) 中被分配的。一个zone是一个能分配内存块的内存系统中的一段虚拟内存区域。一个zone有它自己的空闲(内存)链表以及内存页池(pool of memory pages),从该zone分配出来的内存页实际上仍在这个内存页池中。如果你需要创建很多拥有相同生命周期及访问方式的内存块的话,zone对你来说是很有用的。你可以在zone中分配许多对象空间或内存块,然后通过直接销毁zone的方式直接将这些对象空间和内存块一并销毁,而不是一个个的单独销毁它们。理论上来说,这样使用zone可以减少内存空间的浪费及减少分页操作(reduce paging activity)。但实际上,使用zone的开销基本上也就抵消了使用它带来的上述的优势。 128 | 129 | >注意:在使用malloc方式分配内存时,zone与heap,pool,arena等词汇的意思是一样的。 130 | 131 | 默认情况下,使用malloc函数分配内存一般会发生在默认的zone上(default malloc zone),该zone是你第一次使用malloc时由你的应用程序创建的。尽管经过测算你发现额外申请其它zone并在其上分配内存会为你的应用程序的性能带来提高,但是我们仍然不推荐这样做。例如,如果你发现你的应用中释放大量缓存对象的效率严重的降低了应用性能,那么你可以将它们统一的在一个zone中分配空间,并通过简单的销毁zone一并销毁这些缓存对象,而不是单独一个个销毁,从而达到提升应用性能的目的。当你这么做时,请一定确保在该zone上你的应用的其它数据结构不再持有指向该zone上的对象空间或者内存块的引用指针,因为试图访问一个已经销毁的zone上的内存空间会导致内存错误进而使你的应用崩溃。 132 | 133 | >注意:你绝不能销毁你的应用创建的默认zone空间(default zone) 134 | 135 | 在malloc库层级,zone的相关函数被定义在 **/usr/include/malloc/malloc.h** 可以使用 **malloc_create_zone** 函数创建用户自定义的 malloc zone,或者使用 **malloc_default_zone** 函数获取应用为你创建的 default zone。如果想分配一个特指的zone(particular zone),可以使用 **malloc_zone_malloc, malloc_zone_calloc, malloc_zone_valloc, 或者是malloc_zone_realloc函数** 。如果想释放用户自定义的zone(custom zone),请使用 **malloc_destroy_zone**。 136 | 137 | 138 | ##使用Malloc函数拷贝内存## 139 | 140 | 直接拷贝内存的方式有很多,诸如使用**memcpy**函数或者使用**memove**函数将数据从一个内存块拷贝至另一个数据库。当然拷贝过程中,源内存块和目的内存块都必须同时驻留在内存中,但是这种情况只适合如下几种情况: 141 | 142 | * 你想要拷贝的内存块很小(小于16k)。 143 | * 你想要立刻就使用源内存块或者目的内存块。 144 | * 目的内存块不是页对齐的。 145 | * 源内存块与目的内存块地址重叠。 146 | 147 | 如果你不计划立刻使用源内存块或者目的内存块的话,那么在拷贝大内存块时,使用直接拷贝的方法会很明显的降低你的应用性能。拷贝内存会直接增加你的应用的内存占用空间。一旦你增加了你的应用的内存占用空间,你就增加了页出至硬盘的几率。如果你要对两个大内存块进行拷贝操作(一个源内存块一个是目的内存块),那么很可能这两个大内存块会被页出至硬盘,当你后面真正想要去访问这两个内存块中的某一个的时候,你还需要将它们从硬盘载回内存中,这代价太高了。而使用**vm_copy**函数可以 **延迟拷贝操作** ,从而降低拷贝成本。 148 | 149 | >注意:如果源内存块与目的内存块内存重叠了,你应该使用memove而不是memcpy,在**OS X**中,使用memove能够保重源内存地址与目的内存地址重叠的情况下正确拷贝,但是memcpy不行。(编者注:这是C语言函数实现所决定的,详见memove与memcpy的具体实现) 150 | 151 | ###延迟内存拷贝操作### 152 | 如果你打算拷贝许多内存页,但是你又不打算立刻使用源页或者目的页,那么你也许希望使用**vm_copy**函数来实现这一想法。不像memove或者memcpy,vm_copy并不会真正访问任何物理内存。它只是修改了虚拟内存映射, **使得目的地址空间是源地址空间的一个copy-on-write的版本。** 153 | 154 | 在一些特殊场景下,vm_copy比memcpy要高效的多。当你的代码在拷贝大内存块操作后一段时间不会去访问源内存块以及目的内存块的情况下,使用vm_copy将非常高效。其高效的原因就是vm_copy并不是真正立刻执行拷贝,而是将目的地址空间处理为源地址空间的一个copy-on-write版本,这样就可以延迟拷贝操作的进行。当真正进行该拷贝操作时,内核首先会移除所有虚拟地址空间中指向源地址空间的引用指针。然后进程会访问源页,此时软错误发生(soft fault),内核将物理内存中的该页重新做为copy-on-write页映射到进程的地址空间中来,这样就实现了拷贝。这一处理单一软错误的过程的代价花费与直接拷贝数据的代价差不多。 155 | 156 | ###拷贝小规模数据### 157 | 158 | 如果你需要拷贝小数据量的非内存地址重叠的内存块时,你应该最优先使用**memcpy**而不是其它方法。对于小规模内存块,GCC编译器可以用內联指令来进行值拷贝操作,这样会优化拷贝性能,而如果你使用例如**memove**或者**BlockMoveData**函数,编译器不会对此进行优化。 159 | 160 | ###拷贝数据至Video RAM### 161 | 162 | 当要拷贝数据至VRAM时,请使用 **BlockMoveDataUncached** 函数替代诸如 **bcopy** 等函数。bcopy函数会使用缓存操作指令,这样当拷贝时可能会引起异常操作,内核在此期间必须要修复这些错误,这样会极大降低性能。 163 | 164 | 165 | ##IOS中收到低内存警告时的响应措施## 166 | 167 | IOS中的虚拟内存系统由于不会使用硬盘存储备份空间(笔者注:就是不会将内存页交换至硬盘中去),因此需要依靠与应用程序的合作来移除OC对象的强引用指针。当空闲链表中空闲页的数量低于某一个经过计算的阈值时,系统就会给当前正在运行的应用程序发出低内存警告提示,并且释放内存中那些未经修改的页。如果你的应用程序受到了低内存警告通知,请一定注意。一旦收到该通知,你的应用程序必须尽可能的移除其虚拟内存空间内对象的强引用指针。例如,你可以当收到低内存警告后清除最近新产生的数据缓存等。 168 | 169 | UIKit提供了如下几种接收低内存警告通知的方式: 170 | 171 | * 执行你的应用程序代理函数 :**applicationDidReceiveMemoryWarning:** 。 172 | * 在你继承UIViewCOntroller的子类里面复写 **didReceiveMemoryWarning :** 函数。 173 | * 在默认通知中心中注册通知: **UIApplicationDidReceiveMemoryWarningNotification** 。 174 | 175 | 176 | 如果你的应用只有少量带有已知可清除资源的对象的话,那么你可以为这些对象注册UIApplicationDid ReceiveMemoryWarningNotification通知,一旦收到内存警告,则这些对象主动释放其携带的可清除数据。如果你的应用有很多可清除对象或者你只想选择性的清除其中的几个,那么你可能希望使用你的应用程序代理来决定哪些对象需要释放。 177 | 178 | >重要!!: 179 | > 180 | >即使你在测试环节接收不到低内存警告,你的应用程序也应该像系统应用程序一样总处理低内存警告。当系统低内存情况被检测到时,系统会向所有正在运行的程序(包括你的应用程序)发送低内存警告,同时也可能关闭一些后台应用(如果需要的话)来减轻系统的内存压力。如果你的应用程序因为内存泄漏或者消耗太多内存资源导致系统仍然处于低内存状态,那么系统可能会杀掉你的应用程序。 181 | 182 | 183 | 184 | -------------------------------------------------------------------------------- /缓存和可清除内存.md: -------------------------------------------------------------------------------- 1 | #缓存和可清除内存# 2 | 3 | 对于那些正在处理着需要大量内存及计算时间的对象的应用程序开发者而言,缓存及可清除内存是至关重要的。同样对于那些因内存变满而不得不频繁与硬盘进行页交换操作从而导致性能下降的应用程序而言也是非常重要的。 4 | 5 | ##缓存概述## 6 | 缓存就是一组能够极大提升应用程序性能的对象或者数据的集合。 7 | 8 | ###为什么使用缓存### 9 | 10 | 开发者往往使用缓存去存储一些临时的计算代价高昂的却又被频繁访问的对象。重用这些对象可以带来性能提升。因为这些对象的值不用一次次的被重新计算。但是这些对象对于应用而言又不是必不可缺的,如果内存紧张的话,那么它们会被销毁,如果这些对象被销毁,那么当再次需要将它们加载至内存时,它们的值需要被重新计算出来。 11 | 12 | ###使用缓存可能引发的问题### 13 | 14 | 尽管使用缓存可以带来巨大的性能收益,但是同样因为使用缓存也会带来一些问题。最严重的就是,缓存会占用大量的内存空间。当缓存占用大量内存空间后,可能就不会留给应用程序以及其它对象足够的内存空间,计算机会疲于在物理内存和硬盘之间做页交换操作以释放足够的内存页,从而导致系统性能下降。 15 | 16 | ###解决办法### 17 | 18 | Cocoa提供了一种 **NSCache** 对象来帮助你实现缓存。NSCache类与NSDictionary类非常相似,它们均存储键值对。然而NScache类是一个 **反应式的缓存** ,也就是说当内存够用时,它就缓存所有传给它的数据。但是,当内存不足时,它会自动释放其中的一部分数据从而减少内存占用。最后,如果这些被释放的数据又需要被加载回来后,它们的值还需要重新计算。 19 | 20 | NSCache提供了两个有用的 **“限制”特性** : 21 | 22 | * 可以限制NSCache缓存对象的数量,可以采用函数setCountLimit。例如你设置该NSCache中缓存数量限制为10,你放入了11个对象,那么NSCache就会自动释放其中的某一个对象。 23 | * 可以限制NSCache缓存对象代价的总和,及总代价限制,可以采用函数setTotalCostLimit。你可以为每个对象设置一个代价值,如果添加对象的总代价值大于你设置的代价值上限,那么NSCache就会自动释放其中的某些对象使得NSCache中的总代价降至代价上限之下。这个释放过程不确定,也许是立即释放也会会过一会才释放,一切都依赖于Cache的实现机制。如果你的缓存中的对象均不需要计算代价,那么传值0进去即可,或者使用setObject:forKey函数,该函数不需要传代价值进去。 24 | 25 | >注意:缓存数量限制以及缓存总代价限制并不是严格要求执行的,也就是说当缓存打破了上述的某一个或者全部限制,缓存中的某些对象可能会被立即释放也许会过一段时间才释放,甚至有可能不会释放,这全部取决于缓存内部实现细节。 26 | 27 | 28 | ##使用可清除内存## 29 | 30 | 为了确保你的应用程序不会使用过多内存,Cocoa框架同样也提供了 **NSPurgeableData** 类帮助你解决此问题。NSPurgeableData类遵循 **NSDiscardableContent** 协议,任何实现该协议的对象类都允许在其他对象使用完毕该对象实例后释放内存。当你创建了很多可以随意处置的子控件对象时,你应该为这些对象实现该协议。此外,NSPurgeableData类没必要非得和NSCache类一起使用,你也可以单独使用它实现释放内存的特性。 31 | 32 | ###使用可清除内存的优点### 33 | 34 | 使用 purgeable memory ,你可以使得系统迅速从低内存状态恢复回来,从而提高系统的性能。被标记为可清除标签的的内存是不会被页出至硬盘中的,因为 **分页(paging)** 操作是相当耗时的,取而代之的是直接将该内存块数据销毁,如果后面还需要使用它,那么还需要重新计算该对象的相关数值。 35 | 36 | 在使用 purgeable memory 时需要注意一点,它所占用的那部分内存在访问之前是锁定状态的。这个锁定机制十分有必要,因为它能确保在不会因为其它自动回收机制释放了你正在访问的数据。同样的,该锁定机制也会保证虚拟内存系统没有销毁该数据。NSPurgebleData实现了一个非常简单的锁定机制来保证当它被访问时数据的安全性(数据不会在被访问时销毁)。 37 | 38 | ###如何实现可清除内存### 39 | 40 | NSPurgeableData类使用起来非常简单,因为该类仅仅实现了NSDiscardableContent协议。对于NSDiscardableContent对象的生命周期而言,“计数器”是其核心。当该对象进行读操作时,计数器数值将会大于等于1.当它不再被使用可以被销毁时,计数器数值为0。 41 | 42 | 当计数器值为0时,如果系统内存紧张的话那么该内存块可能会被销毁。为了销毁该内存块数据,会调用 **discardContentIfPossible**函数,该函数会释放数据所在的相关计数器数值为0的内存区域。 43 | 44 | 默认情况下,当一个NSPurgeableData对象初始化的时候,它的引用计数变量值为1,并且可以被安全的访问。为了访问这个purgeable memory,调用 beginContentAccess方法即可。这个方法首先会检查这个对象的数据是否被销毁。如果这个数据仍然在,它将会增加这个对象指向的内存的引用计数,并且返回YES。如果这个对象的数据已经被销毁了,这个方法就会返回NO。当我们完成对这个对象的访问后,调用endContentAccess方法即可,这个方法会减少这块内存区域的引用计数,并允许系统在内存紧张时释放它。只有当 beginContentAccess方法返回YES时,我们才可以去访问这个对象所指向的内存空间。 当系统可用内存减少时,系统或客户端对象通过调用discardContentIfPossible方法来销毁purgeable数据,当对象所指向的内存引用计数为0时,这个方法仅仅会销毁上面的数据,然后不再做其它任何操作。如果这个内存被销毁了,那么 isContentDiscarded方法会返回YES。下面是一个使用NSPurgeableData对象的例子: 45 | 46 | NSPurgeableData * data = [[NSPurgeableData alloc] init]; 47 | [data endContentAccess]; //Don't necessarily need data right now, so mark as 48 | discardable. 49 | //Maybe put data into a cache if you anticipate you will need it later. 50 | ... 51 | if([data beginContentAccess]) 52 | { //YES if data has not been discarded and counter variable has been incremented 53 | ...Some operation on the data... 54 | [data endContentAccess] //done with the data, so mark as discardable 55 | } 56 | else 57 | { 58 | Caching and Purgeable Memory Using Purgeable Memory 59 | //data has been discarded, so recreate data and complete operation 60 | data = ... 61 | [data endContentAccess]; //done with data 62 | } 63 | //data is able to be discarded at this point if memory is tight   64 | 65 | ###Purgeable Memory 和 NSCache### 66 | 67 | 当一个实现了 NSDiscardableContent协议的对象放在NSCache对象中去时,cache对象就维持了一个对该对象的强引用指针。如果这个对象的内容已经被销毁(discard)了,这个缓存的evictsObjectsWith DiscardedContent的值将会被设置为YES,然后这个对象会自动从缓存中移除。   68 | 69 | ###关于使用Purgeable Memory的一些警告### 70 | 71 | 值得注意的是Purgeable memory是针对那些大对象或大内存块才可以直接使用的内存。Purgeable API也是作用于多页大小虚拟内存对象,这就使得我们很难把一个很小的缓存元素标记为purgeable。cache API会做一些必要的记录使得较小的缓存元素也可以使用purgeable memory。同样的,有些情况下,非要通过cache API为某些缓存元素分配内存空间也是不合适的,当我们可以十分方便的创建一个对象,或这个对象已经在不同层(layer)分配内存控件了,并且这个层(layer)已经对它做了缓存。在这些情况下,就没必要使用purgeable memory。 72 | 73 | ###什么时候使用Purgeable Memory### 74 | 75 | 当清除对象的开销小于分页(paging)的开销时,我们可以考虑采用Purgeable Memory,即分页(paging)的开销远大于再次使用这个对象时重新计算相关数据值的性能开销。很多时候缓存没没有遵循上面的规律,它们的一些缓存项很有可能以后都不会再次使用。同样的,那些能够轻松计算出数据的缓存项可以考虑使用purgeable memory,因为重新计算这些数据并不会耗费应用程序多少性能。 -------------------------------------------------------------------------------- /虚拟内存系统.md: -------------------------------------------------------------------------------- 1 | #关于系统虚拟内存# 2 | 3 | 对于**OS X**以及**IOS**而言,使用高效的内存管理技术是书写高水平代码的重要表现之一。最小化你的内存使用不仅可以降低你的程序的内存占用水平,同时也可以减少因此带来的CPU的相关耗费。为了能够更好的优化你的代码,你必须明白系统底层是如何管理内存的。 4 | 5 | **OS X**以及**IOS**都包含了完整集成的虚拟内存系统,用户无法主动关闭它,它一直是开启状态。**OS X**及**IOS**针对32位进程均提供了高达4GB的内存地址访问空间。此外**OS X**还针对64位进程提供了高达18EB的内存地址访问空间。即使计算机的内存大于4GB,系统也鲜会给单个进程分配大于4GB的内存空间。 6 | 7 | 为了能够给足单个进程所能分配的最大内存空间(32位是4GB,64位是18EB),**OS X**使用硬盘去存储那些目前不再使用的数据。随着内存逐渐占用变满,部分不再使用的内存数据将会被写入硬盘,从而为需要写入内存的新的数据腾出空间。那些存储暂时不再使用数据的硬盘空间被称为备份存储空间,因为它为内存提供了备份支持。 8 | 9 | 尽管**OS X**提供了备份存储支持,但是**IOS**却并没有提供。在Iphone的app应用中,已经存储在硬盘上的只读数据(例如存储代码的页)只会当系统需要时直接从硬盘中加载入内存,当系统不需要时直接从内存中释放。可写数据是绝对不会被操作系统从内存中移除的。与之对应的是,如果**IOS**可用内存低于某一阈值,系统会要求正在运行的app应用程序为即将加入内存的新数据释放内存空间。如果该app没有及时释放足够的内存,那么系统将会直接终结掉该app。 10 | 11 | >注释:不同于基于UNIX的大多数系统,**OS X**不会使用预先分配的硬盘空间做备份存储而是使用系统根目录下的所有空间作为可能的备份存储空间。 12 | 13 | 下面几节将介绍相关术语并对在**OS X**以及**IOS**中使用的虚拟内存系统做一个简要介绍。如果想了解更多有关虚拟内存的工作原理,请阅读《*Kernel Programming Guide*》。 14 | 15 | ##关于虚拟内存## 16 | 虚拟内存使得操作系统摆脱物理内存(RAM)大小限制所带来的束缚。虚拟内存管理器(VVM)为每个进程创建了一种称作逻辑地址的空间(或者被称为“虚拟”地址空间),并将这些地址空间分割成了大小一致的块,这样的块称之为“**页**”。进程还有它的内存管理单元(MMU)维护了一个页表,该页表将程序的逻辑内存地址空间和实际的物理内存地址空间做了映射。当程序代码访问内存地址(逻辑地址)时,内存管理单元会负责将逻辑地址转换为实际的物理地址。这种转换是自动进行的并对正在运行的app是透明的。 17 | 18 | 作为一个程序而言,它的逻辑地址空间中的地址总是可用的(可访问)。然而,如果一个应用app访问了一个没有在物理内存中有映射关系的逻辑内存页,那么**页错误**就产生了。当这种错误产生时,虚拟内存系统会立刻唤起一个特殊的页错误处理器来响应这个错误。这个页错误处理器会首先暂停目前正在运行的代码,然后在物理内存中开辟一页新的空闲内存空间,将需要从硬盘中载入内存的数据写入该页中,然后更新页表,最后将控制权重新移交给程序代码,从而使的程序代码能够正常访问内存地址(逻辑内存地址)。这个过程我们称之为**分页(Paging)**。 19 | 20 | 当然,如果物理内存空间没有足够的空闲页的话,页错误处理器必须首先在物理内存中释放一个已经被占用的页来换得新的空闲页。系统如何释放页取决于系统平台。对于**OS X**而言,虚拟内存系统通常会将页写入硬盘中的备份存储空间。这个备份存储空间备份了内存中的页的数据信息。将数据从物理内存移动至备份存储空间的过程称之为**页出(Page Out)**。将数据从备份存储空间载入内存的过程称之为**页入(Page in)**。在**IOS**中,由于没有备份存储机制,所以页从来不会从内存交换至硬盘中去,但是当需要时只读页还是会被从硬盘载入至内存中去的。 21 | 22 | 无论**OS X**或者**IOS**,页的大小均为4K。因此,每发生一次页错误,系统都会从硬盘中读取4K数据。当系统花费了大量时间处理页错误以及页的读写操作而不是用来处理程序代码时,就会产生磁盘震颤( *disk thrashing* )现象。 23 | 24 | 任何形式的**分页(Paging)**,以及特别是磁盘震颤现象都会对系统性能产生消极影响,因为它们会迫使系统花费大量时间进行磁盘读写。从备份存储空间读取数据所耗费时间要远高于直接从内存中读取数据。此外,如果系统在从硬盘中读取某一页数据之前必须要先写入一页数据,那么对于系统性能造成的影响将会更糟。 25 | 26 | ##虚拟内存系统细节介绍## 27 | 28 | 一个进程的逻辑地址空间包含着若干物理内存映射区域。每个物理内存映射区域都又包含若干已知数量的虚拟内存页。每个物理内存映射区域都通过一些特殊的属性标签将这些页分为不同的类型,例如是否是写保护,或者是否是关联内存页(如果是的话,那么它将不会执行页出操作)。又因为每个物理内存区域包含有已知数量的页,所以每个区域都是**页对齐( *page-aligned* )**的,这意味着这个物理内存映射区域的首地址即为页的首地址,物理内存区域的末地址即为页的末地址。 29 | 30 | 内核将每个逻辑地址空间区域与一个VM(虚拟内存)对象关联到一起。内核使用VM对象去追踪管理关联内存区域中留存在内存和暂时留存在内存中的页。一个内存区域可以映射备份存储空间中的某一部分,也可以映射文件系统中的某些文件。每一个VM对象都会将其对应的内存区域与**默认分页器(default pager)**或者**vnode分页器(vnode pager)**建立关联。**默认分页器**是指管理着被页出至备份存储区域的虚拟内存页并且当系统需要时重新将这些页写回内存的管理器。**vnode分页器**使用分页机制提供了一个直接面向文件的窗口。这个机制使得当文件驻存在内存中时,你可以读写器中的某些部分。 31 | 32 | 除了将内存区域映射至**默认分页器**或者**vnode分页器外**,VM对象也可能将区域映射至另一个VM对象。内核使用这种自引用技术来实现*copy-on-write*内存区域。该内存区域允许多进程(或者一个进程中的多个blok)在不进行更改写入操作的前提下共享一个页面,当某一个进程试图对该页进行写入操作时,该进程就会在其逻辑地址空间对该页进行拷贝,创建一个拷贝页来进行写入操作。自此,执行写入操作的进行就会维护该拷贝页使得它可以在任何时间进行写入操作。*copy-on-write*内存区域使得系统内存共享了大量数据,且又可以保证不同进程对某内存进行直接又安全的操作。通常系统框架载入的内存区域往往是上述所说的内存区域类型。 33 | 34 | 每一个VM对象都包含有如下字段,如下所示: 35 | 36 | * Resident pages : 该区域内常驻物理内存的页的链表 37 | * Size : 该区域大小 38 | * Shadow : 用于copy-on-write优化 39 | * Copy : 用于copy-on-write优化 40 | * Attributes : 该区域执行不同操作的标记 41 | 42 | 如果VM对象涉及到*copy-on-write*操作,那么shadow和copy字段会指向另一个VM对象。否者这两个字段都会指向NULL。 43 | 44 | ##关联内存## 45 | 46 | 关联内存(也叫驻留内存)通常存储内核代码和相关数据结构,这部分内存是绝对不会被页出至硬盘中去的。应用,架构,或者其他用户层级的软件是不能够申请关联内存的。然而,它们却可以影响某一时刻有多少关联内存存在。例如,一个会隐式创建数条线程和端口的应用就会影响到关联内存,因为创建线程和端口都会需要系统内核资源,这其中就包含着关联内存。 47 | 如下列出了一个应用程序产生导致相应的关联内存耗费关系: 48 | 49 | * 进程 : 16k 50 | * 线程 : blocked in a continuation 5k; blocked 21K 51 | * Mach 端口 : 116B 52 | * Mapping : 32B 53 | * Library :2K + n*200B (n代表使用该Library的task数量) 54 | * Memory region : 160B 55 | 56 | >注意:这些数据可能会随着操作系统的升级而发生改变,列在这里是给你一个直观的耗费关系。 57 | 58 | 正如你所看到的,每一个线程,每一个进程,每一个library都对系统造成了内存占用。除了你的应用不同程度的使用者关联内存,系统内核自己的一些实体也需要关联内存,如: 59 | 60 | * VM对象 61 | * 虚拟内存缓存 62 | * I/O缓存 63 | * 驱动 64 | 65 | 关联内存同样也关联着物理内存页面以及对应的页表。上述这些实体也会随着可用物理内存的增加而增加。当你为系统增加内存后,及时没有做任何其它改变,关联内存的大小也会增加。当电脑第一次启动加载至Mac的Finder下,没有任何其它应用此时在运行。关联内存也会大概占用64兆系统大小中的14兆,128兆系统大小中的17兆。 66 | 67 | 当关联内存页无效时,它们不会立刻被移动至空闲链表中去,相对应的,它们会当因空闲页面数值低于阈值触发页出操作时,被当作“垃圾回收走”。 68 | 69 | ##内核中的页链表## 70 | 71 | 内核维护并且时常访问三条系统级别的物理内存链表: 72 | 73 | * **活跃(内存)链表**,它主要包含那些经常被映射到内存中去,经常被访问的页。 74 | * **非活跃(内存)链表**,它主要包含那些经常驻留在内存里,但是最近已经没有被访问的页。这些页中都包含着有效的数据,但是它们随时可能会从内存中被删除掉(OS X会转移到硬盘,IOS会要求用户处理,否则就直接删除) 75 | * **空闲(内存)链表**,包含那些没有被虚拟内存地址所关联的空闲的物理内存页。这些页在系统需要它们的时候可以立即被使用。 76 | 77 | 当**空闲链表**的可用页数小于某个阈值时(主要由物理内存的大小决定),分页器就会试图去平衡上述3个链表所包含的页数(实际意思是就开始要增加**空闲链表的长度**,提升可用的空闲页的数量),采取方法的途径就是从**非活跃链表**中取出非活跃页填补至空闲页。如果**非活跃链表**中某一页最近又被访问了,那么它将重新激活为活跃页,并被放入**活跃链表**的尾端。在**OS X**平台下,如果一个包含数据的非活跃页最近没有被写回至存储备份区域,那么它必须被执行**页出**操作,将数据写至硬盘的存储备份区域,然后它将会被放置到**空闲链表**中去(在**IOS**中,已经修改过的非活跃页必须驻留在内存中,由应用程序自己清理掉它)。如果一个非活跃页没有被修改且又不是关联内存类型的页的话,那么对应它的虚拟内存映射将会被销毁,然后被添加至**空闲**链表中去。一旦空闲链表中空闲页数量超过了最低阈值,此时分页器停止工作。 78 | 79 | 内核同样会将没有被访问的页从**活跃链表**移动至**非活跃链表**中去;当发生[**软错误(soft fault)**](1)时,内核会将页从**非活跃链表**移动至**活跃链表**。当内存中的逻辑地址页(虚拟页)被从内存中交换出至硬盘中时,其对应的物理页将被放置在**空闲链表**中去,当然,当进程显式地释放空闲内存时,内核同样会进行上述操作。 80 | 81 | ##页出过程## 82 | 83 | 在**OS X**平台下,当**空闲链表**中的空闲页数量低于某个经过计算的阈值后,内核会将**非活跃链表**中的页交换出内存,从而为**空闲链表**页提供可用的物理内存页。为了实现上述操作,内核会迭代所有**活跃链表**及**非活跃链表**中的页,并执行如下操作: 84 | 85 | 1. 如果某一个活跃链表中的页最近没有被访问,那么它将被移动至非活跃链表中。 86 | 2. 如果某一个非活跃链表中的页最近没有被访问,那么内核将会找到该页对应的VM对象。 87 | 3. 如果该VM对象在此之前从来没有被**分页(paged)**,内核会创建并指派给该VM对象一个**默认分页器**。 88 | 4. 这个VM对象的**默认分页器**会尝试将该VM对象下的第2步操作所述的页进行页出操作至硬盘中的备份存储区域。 89 | 5. 如果分页器执行操作成功,内核会释放该页所对应的物理内存,并将该页从非活跃链表移动至空闲链表中去。 90 | 91 | >提示:在IOS中,内核不会执行将页换出至备份存储区域的操作,当空闲链表中的空闲页数量低于某个经过计算的阈值后,内核会刷新那些身在非活跃链表且未经修改的页,也可能会要求应用程序本身直接执行释放操作。更多的信息请看[IOS的低内存警告应对措施](2)。 92 | 93 | ##页入过程## 94 | 95 | 虚拟内存管理的最后一个步骤就是将页从硬盘备份存储区域或者包含页数据的文档中重新移动至物理内存中来。一般内存访问错误会激活页入操作进程。当代码试图访问一个没有与物理内存建立映射关系的虚拟内存地址时,内存访问错误就会发生。 96 | 97 | 有如下两种错误类型: 98 | 99 | * **软错误(soft fault)**,当该页的相关地址驻留在物理内存中,但是目前却没有映射至该进程的地址空间中,这时候访问该页虚拟内存地址发生的错误叫做软错误。 100 | * **硬错误(hard fault)**,当该页的相关地址没有在物理内存中,而是被页出至硬盘存储备份区域。这时候访问该页虚拟内存地址发生的错误叫做硬错误。 101 | 102 | 无论上述哪种错误发生,内核会锁定错误发生的页的映射入口和该页所在区域对应的VM对象。然后内核会遍历该VM对象的驻留物理内存的页链表。如果目标页在该驻留内存页链表上,内核即产生**软错误(soft fault)**。反之,如果目标页不再驻留内存页链表上,那么内核产生**硬错误(hard fault)**。 103 | 104 | 对于**软错误(soft fault)**,内核会将包含该页的物理内存与该进程的虚拟地址空间建立映射,然后内核将该页标记为活跃页。如果发生软错误的页还涉及到了写操作,那么该页也会被标记为 *被修改页* ,这样,如果将来它被释放的时候会先写入硬盘中的备份存储空间中去。 105 | 106 | 对于**硬错误(hard fault)**,VM对象**分页器(Pager)**会依据该页的类型在硬盘中的备份存储区域找到该页的物理存储地址。然后对VM对象的映射信息作出适当修改后,就会将该页重新移回至物理内存,将该页放入**活跃(内存)链表**。与软错误一样,如果发生错误的页还涉及到写操作,则该页会被标记为 *被修改页* 。 107 | 108 | --------------------------------------------------------------------------------