内存分配之堆和栈(内存管理一)
这世上,没有谁活得比谁容易,只是有人在呼天抢地,有人在默默努力。
随着科技的发展,移动设备的内存越来越大,设备的运行速度也越来越快,但是相对于整个应用市场上成千上万的应用容量来说,还是及其有限的。因此,每一个应用所能占用的内存是有限制的。这一专题就是来探讨系统中的内存是如何分配的。
一. 内存分配的引入
1. 什么行为会增加App的内存占用?
① 创建一个OC对象。
② 定义一个变量。
③ 调用一个函数和方法。
2. 内存管理范围
任何继承了NSObject的对象,其它非对象类型不需要管理。简单来说,只有OC对象需要内存管理,非OC对象类型不需要内存管理,比如基本数据类型。
3. 引入堆和栈的概念
由内存管理范围,我们是不是就有疑问了?为什么OC对象需要进行内存管理,而其它非对象类型比如基本数据类型就不需要进行内存管理了呢?只有OC对象才需要进行内存管理的本质原因是什么?
因为OC的对象在内存中是以堆(heap)的方式分配空间的,并且堆(heap)是由程序开发者释放的,就是release。也就是说OC对象是存储在堆(heap)里面的,堆内存需要程序开发者手动回收。而非OC对象一般存放在栈里面,栈内存会被系统自动回收。堆内存是动态分配的,所以也需要程序开发者手动添加内存,回收内存。
二. 内存分配区
所有的进程(执行中的程序)都必须占用一定数量的内存,它或许是用来存放从磁盘载入的程序代码,或是存放取自用户输入的数据等等。不过进程对这些内存的管理方式因内存用途不一而不尽相同,有些内存是事先静态分配和统一回收的,而有些却是按需要动态分配和回收的。
1. 代码区
代码段是用来存放可执行文件的操作指令(存放函数的二进制代码),也就是说它是可执行程序在内存中的镜像。代码段需要防止在运行时被非法修改,所以只允许读取操作,而不允许写入操作。它是不可写的。
2. 常量区
常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量。编译时分配,APP结束时由系统释放。
3. 全局(静态)区
编译时分配,APP结束时由系统释放。
-
数据区,数据段用来存放可执行文件中已经初始化的全局变量,也就是用来存放静态分配的变量和全局变量。
-
BSS区,BSS段包含了程序中未初始化的全局变量。
4. 堆(heap)区
堆(FIFO)是由程序开发者分配和释放,用于存放进程运行中被动态分配的内存段,它大小并不固定,可动态扩张或缩减。当进程调用alloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用release释放内存时,被释放的内存从堆中被剔除(堆被缩减),因为我们现在iOS基本都使用ARC来管理对象,所以不用我们程序员来管理,但是我们要知道这个对象存储的位置。
5. 栈(stack)区
栈(LIFO)是由编译器自动分配并释放,用户存放程序临时创建的局部变量,存放函数的参数值,局部变量等。也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味这在数据段中存放变量)。除此以外在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进后出(LIFO)特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上将我们可以把栈看成一个临时数据寄存、交换的内存区。
上述几种内存区域中常量区、数据段、BSS和堆通常是被连续存储的,内存位置上是连续的,而代码段和栈往往会被独立存放。
栈是向低地址扩展的数据结构,是一块连续的内存的区域。堆是向高地址扩展的数据结构,是不连续的内存区域。有人会问堆和栈会不会碰到一起,他们之间间隔很大,绝少有机会能碰到一起,况且堆是链表方式存储。
int age = 27; //全局初始化区(数据区)
NSString *name; //全局未初始化区(BSS区)
static NSString *sName = @"Dely";//全局(静态初始化)区(数据区)
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Int tmpAge;//栈
NSString * tmpName = @"Dely";//栈
NSString * number = @"123456"; //123456\0在常量区,number在栈上。
NSMutableArray * array = [NSMutableArray arrayWithCapacity:1];//分配而来的8字节的区域就在堆中,array在栈中,指向堆区的地址。
NSInteger total = [self getTotalNumber:1 number2:1];
}
// 当ViewDidLoad代码块一过,tmpAge、tmpName 、number 、* array指针都会被系统编译器自动回收。而OC对象不会被系统回收,因为它存放在堆里面,堆里面的内存是动态存储的,所以需要程序员手动回收内存。
-(NSInteger)getTotalNumber:(NSInteger)number1 number2:(NSInteger)number2{
return number1 + number2;//number1和number2 栈区
}
@end
三. 堆和栈的区别
1. 申请方式和回收方式
-
栈区(stack):由编译器自动分配和释放。
-
堆区(heap):由程序开发者分配和释放。
2. 申请后的系统响应
- 栈:每一个函数在执行的时候都会向操作系统索要资源,栈区就是函数运行时的内存,栈区中的变量由编译器负责分配和释放,内存随着函数的运行分配,随着函数的结束而释放,由系统自动完成。
注意:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
- 堆:首先应该知道操作系统有一个记录空闲内存地址的链表。当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中
3.申请大小的限制
-
栈:栈是向低地址扩展的数据结构,是一块连续的内存的区域。是栈顶的地址和栈的最大容量是系统预先规定好的,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数 ) ,如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
-
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
4.申请效率的比较
-
栈区(stack):由系统自动分配,速度较快。但程序员是无法控制的。
-
堆区(heap):是由alloc分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。
5.分配方式的比较
-
栈区(stack):有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloc函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。
-
堆区(heap):堆都是动态分配的,没有静态分配的堆。
6.分配效率的比较
-
栈区(stack):栈是操作系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。
-
堆区(heap):堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。