问题
目录
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
: 常量。编译期会
进行语法识别
,需要指定类型
。编译期不
会分配内存
,仅在第一次
使用时,开辟内存
并记录
内存地址
。后续
调用时不
会开辟内存
,直接返回
记录的内存地址
。效率
更快
。内存
占用更少
。
6:内存5大区代码验证
可以通过以下代码,加深印象:
- (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:静态区安全测试
静态变量的作用范围是当前文件内。
- 当前文件更改静态变量后,本文件内再访问,是更改后的值,但不影响别的文件中的这个静态变量的值。
- 别的文件引入静态变量后,拿到的是静态变量的初始值,修改后再访问是自己修改后的值。
- 也就相当于引用别的文件时,底层会深拷贝一份静态变量,放在了自己的文件中,以后访问及操作的都是本文件内的这个变量,对别的文件没有影响。
注意