019*:内存五大区:(栈、堆、全局静态区、常量区、代码区)(线程、函数栈、栈帧)
问题
目录
1:内存基础概念
2:内存五大区
3:函数栈
预备
正文
一、内存基础概念
1.1 物理内存 & 虚拟内存
- 物理内存(Physical Memory):指通过物理内存条而获得的内存空间,和虚拟内存对应;主要作用是:设备运行时为操作系统和各种程序提供临时储存空间;iPhone 6 和 6 Plus 及之前都是 1G 内存、iPhone XS Max 和 11 Pro 是 4GB 内存,目前比较新的iPhone 12 Pro 是 6GB 内存;
- 虚拟内存(Virtual Memory):是计算机系统内存管理的一种技术,为每一个进程提供了一个 一致的、私有的地址空间;其主要作用是:保护了每个进程的地址空间不会被其他进程破坏,降低内存管理的复杂性;32位设备虚拟内存大小是4GB,64位设备(5s以后的设备)是 4GB * 4GB;
- 虚拟内存是进程运行时所有内存空间的总和,并且可能有一部分不在物理内存中;
1.2 段页式存储
- 目前,大部分通用的计算机的内存管理使用 段页式存储结构;用户程序先分段,每个段内再分页;而 页是存储的最基本单位,iOS设备的 arm64 架构后,页大小是16KB;
- 利用 逻辑地址(段号 + 段内页号 + 页内地址) 进行地址变化,获得物理地址;这样的话,在段页式结构中,须三次访问内存才能获取数据或指令;
- 当进程访问一个虚拟内存的页时,而对应的物理内存却不存在时,会触发一次 Page Fault(缺页中断),将需要的数据 or 指令从磁盘加载到物理内存页中,建立映射关系,然后再恢复现场,程序本身是无感知的;
二:内存5大区
按照地址从高到低排列: 栈区 -> 堆区 -> 全局静态区 -> 常量区 -> 代码区 (内核区和保留部分不再考虑范围内)


补充说明:
内存五大区,实际是指虚拟内存,而不是真实物理内存。iOS系统中,应用的虚拟内存默认分配4G大小,但五大区只占3G,还有1G是五大区之外的内核区
1:栈区(Stack)
1.1:栈区特点
- 函数内部定义的
局部变量和数组,都存放在栈区; (比如每个函数都有的(id self, SEL _cmd)) - 栈区的
内存空间由系统管理。(函数调用时开辟空间,函数调用结束时回收空间) - 栈是从
高地址向低地址扩展,是一块连续的内存区域,遵循FILO先进后出原则,效率高。 - 栈区一般在
运行时进行分配 栈的地址空间在iOS中通常以0x7开头
栈区空间大小较小,所以空间比较宝贵,但是读取写入效率高,下面我们看看栈区都会存储什么内容
1.2:存储
栈区一般是由编译器来自动分配和释放的,主要用来存储一下内容
局部变量函数的参数,例如函数的隐藏参数(id self,SEL_cmd)
1.3:优缺点
- 优点
- 不会产生内存碎片(回收释放有系统自己控制)
- 高效的读写速度
- 缺点
- 栈的内存较小(iOS主线程栈大小1MB,其它线程512KB)
- 存储数据不灵活(存储内容基本固定,由编译器分配)
1.4:缓冲区域
栈区和堆区中间有小块未使用的内存区域。用于给栈区和堆区之间创建一个缓冲区域
- 溢出:
到达缓冲区的数据向小缓冲区复制的过程中,由于没有注意小缓冲区的边界,导致小缓存区满了,从而覆盖了和小缓存区相邻内存区域的其他数据而引起的内存问题。
(就像桶盛水,水多了,自然越界溢出来了。)
2:堆区
2.1:定义
- 1.堆是从
低向高地址扩展的数据结构 - 2.堆是
不连续的内存区域,类似于链表结构,遵循先进先出原则:FIFO - 3.堆的地址空间在iOS中
通常以0x6开头 - 4.堆区的分配一般也是在
运行时进行分配的
2.2:存储
堆区一般是由开发者自己分配和释放的,同时系统也会在必要的时候对堆区存储的内容进行回收和释放(系统检测属性或者对象引用计数为零时,进行回收)
OC使用alloc、new、block或者使用copy创建的对象都会存在这里(ARC下编译器会自动在合适的时候释放内存,而在MRC下需要开发者手动释放)- C语言中使用malloc、calloc、realloc分配的空间(需要free释放)
2.3:优缺点
- 优点
- 使用灵活方便,数据使用更加广泛
- 缺点
- 内存需要手动管理
- 容易产生碎片
- 读取速度和栈区比较慢
访问堆区内存时,一般是先通过对象读取到对象所在的栈区的指针地址,然后通过指针地址访问堆区
需要注意:
野指针:提前释放了,查询时找不到内容内存泄露:没有释放,一直占用内存过度释放:对已释放的对象进行release操作。
3:全局区(静态区,即.bss & .data)
全局区是编译时分配的内存空间,在iOS中一般以0x1开头,在程序运行过程中,此内存中的数据一直存在,程序结束后由系统释放,主要存放
-
未初始化的全局变量和静态变量,即BSS区(.bss) -
已初始化的全局变量和静态变量,即数据区(.data)
其中,全局变量是指变量值可以在运行时被动态修改,而静态变量是static修饰的变量,包含静态局部变量和静态全局变量
static修饰的变量仅执行一次,生命周期为整个程序运行期
4:常量区(即.rodata)
常量区是编译时分配的内存空间,在程序结束后由系统释放,主要存放
- 已经使用了的,且没有指向的
字符串常量
字符串常量因为可能在程序中被多次使用,所以`在程序运行之前就会提前分配内存
存放常量(整型、字符型,浮点,字符串等),整个程序运行期不能被改变。
空间由系统管理,生命周期为整个程序运行期。
5:代码区(即.text)
代码区是编译时分配主要用于存放程序运行时的代码,代码会被编译成二进制存进内存的
存放程序执行的CPU指令。(编译期将代码转换为CPU指令)
define和const区别:
define: 宏。编译期不会进行语法识别,没有类型。编译期会分配内存。每次使用都会进行宏替换和开辟内存。
const: 常量。编译期会进行语法识别,需要指定类型。编译期不会分配内存,仅在第一次使用时,开辟内存并记录内存地址。后续调用时不会开辟内存,直接返回记录的内存地址。效率更快。内存占用更少。
- (void)test { NSInteger i = 666; NSLog(@"NSInteger i -> 内存地址:%p", &i); // 【局部变量】 栈区 NSString * name = @"HT"; NSLog(@"NSString name -> 内存地址: %p", name); // 【字符串内容】 存放在常量区 NSLog(@"NSString name -> 指针地址: %p", &name);// 【局部变量name的指针】 存放在栈区 NSObject * objc = [NSObject new]; NSLog(@"NSObject objc -> 内存地址: %p", objc);// 【对象的内容】 存放在堆区 NSLog(@"NSObject objc -> 指针地址: %p", &objc);//【对象的指针】 存放在栈区 }
打印结果: (0x7开头: 栈区 、 0x1开头: 常量区、 0x6开头: 堆区)
- 对于
局部变量i,从地址可以看出是0x7开头,所以i存放在栈区 - 对于
字符串对象string,分别打印了string的对象地址和string对象的指针地址-
string的
对象地址是以0x1开头,说明是存放在常量区 -
string
对象的指针地址是以0x7开头,说明是存放在栈区
-
- 对于
alloc创建的对象obj,分别打印了obj的对象地址和obj对象的指针地址(可以参考前文的汇总图)-
obj的
对象地址是以0x6开头,说明是存放在堆区 -
obj
对象的指针地址是以0x7开头,说明是存放在栈区
-
三:函数栈
函数栈又称为栈区,在内存中从高地址往低地址分配,与堆区相对,具体图示请查看文章最开始的图示
栈帧是指函数(运行中且未完成)占用的一块独立的连续内存区域
应用中新创建的每个线程都有专用的栈空间,栈可以在线程期间自由使用。而线程中有千千万万的函数调用,这些函数共享进程的这个栈空间。每个函数所使用的栈空间是一个栈帧,所有的栈帧就组成了这个线程完整的栈
函数调用是发生在栈上的,每个函数的相关信息(例如局部变量、调用记录等)都存储在一个栈帧中,每执行一次函数调用,就会生成一个与其相关的栈帧,然后将其栈帧压入函数栈,而当函数执行结束,则将此函数对应的栈帧出栈并释放掉
-
其中
main stack frame为调用函数的栈帧 -
func1 stack frame为当前函数(被调用者)的栈帧 -
栈底在高地址,栈向下增长。 -
FP就是栈基址,它指向函数的栈帧起始地址 -
SP则是函数的栈指针,它指向栈顶的位置。 -
ARM压栈的顺序很是规矩(也比较容易被黑客攻破么),依次为当前函数指针PC、返回指针LR、栈指针SP、栈基址FP、传入参数个数及指针、本地变量和临时变量。如果函数准备调用另一个函数,跳转之前临时变量区先要保存另一个函数的参数。 -
ARM也可以
用栈基址和栈指针明确标示栈帧的位置,栈指针SP一直移动,ARM的特点是,两个栈空间内的地址(SP+FP)前面,必然有两个代码地址(PC+LR)明确标示着调用函数位置内的某个地址。
堆栈溢出
一般情况下应用程序是不需要考虑堆和栈的大小的,但是事实上堆和栈都不是无上限的,过多的递归会导致栈溢出,过多的alloc变量会导致堆溢出。
所以预防堆栈溢出的方法:
(1)避免层次过深的递归调用;
(2)不要使用过多的局部变量,控制局部变量的大小;
(3)避免分配占用空间太大的对象,并及时释放;
(4)实在不行,适当的情景下调用系统API修改线程的堆栈大小;
栈帧示例
描述下面代码的栈帧变化
栈帧程序示例
int Add(int x,int y) { int z = 0; z = x + y; return z; } int main() { int a = 10; int b = 20; int ret = Add(a, b); }
程序执行时栈区中栈帧的变化如下图所示

四:内存如何分配
1:栈区地址如何分配?
- (void)testStack{ NSLog(@"************栈区************"); // 栈区 int a = 10; int b = 20; NSObject *object = [NSObject new]; NSLog(@"a == \t%p",&a); NSLog(@"b == \t%p",&b); NSLog(@"object == \t%p",&object); }

由上图的打印结果可以看出:
- 局部变量的地址在栈区
- 栈区的地址分配时,是高地址 -> 低地址
2:堆区地址如何分配?
- (void)testHeap{ NSLog(@"************堆区************"); // 堆区 NSObject *object1 = [NSObject new]; NSObject *object2 = [NSObject new]; NSObject *object3 = [NSObject new]; NSObject *object4 = [NSObject new]; NSObject *object5 = [NSObject new]; NSObject *object6 = [NSObject new]; NSObject *object7 = [NSObject new]; NSObject *object8 = [NSObject new]; NSObject *object9 = [NSObject new]; NSLog(@"object1 = %@",object1); NSLog(@"object2 = %@",object2); NSLog(@"object3 = %@",object3); NSLog(@"object4 = %@",object4); NSLog(@"object5 = %@",object5); NSLog(@"object6 = %@",object6); NSLog(@"object7 = %@",object7); NSLog(@"object8 = %@",object8); NSLog(@"object9 = %@",object9); // 访问---通过对象->堆区地址->存在栈区的指针 }

由上图的打印结果可以看出:
- 局部变量的地址在栈区,对象的地址在堆区
- 栈区的地址分配时,是低地址 -> 高地址
3:BSS段地址如何分配?
int clA; int clB = 10; static int bssA; static NSString *bssStr1; static int bssB = 10; static NSString *bssStr2 = @"bss"; - (void)testConst{ NSLog(@"************BSS段************"); NSLog(@"clA == \t%p",&clA); NSLog(@"bssA == \t%p",&bssA); NSLog(@"bssStr1 == \t%p",&bssStr1); NSLog(@"***********DATA段************"); NSLog(@"clB == \t%p",&clB); NSLog(@"bssB == \t%p",&bssB); NSLog(@"bssStr2 == \t%p",&bssStr2); }
由上图的打印结果可以看出:
- 未初始化话的全局变量和静态变量,在BSS段
- BSS段的地址分配时,是低地址 -> 高地址
- 已初始化话的全局变量和静态变量,在DATA段
- DATA段的地址分配,与变量定义的顺序无关
4:静态区安全测试
静态变量的作用范围是当前文件内。
- 当前文件更改静态变量后,本文件内再访问,是更改后的值,但不影响别的文件中的这个静态变量的值。
- 别的文件引入静态变量后,拿到的是静态变量的初始值,修改后再访问是自己修改后的值。
- 也就相当于引用别的文件时,底层会深拷贝一份静态变量,放在了自己的文件中,以后访问及操作的都是本文件内的这个变量,对别的文件没有影响。
注意

浙公网安备 33010602011771号