OC底层原理24:内存五大区

张建 lol

前言

在iOS中,内存主要分为 栈区、堆区、全局区、常量区、代码区 五个区域,如下图所示:

栈区

定义

  • 栈是 系统数据结构,其对应的 进程或者线程是唯一的

  • 栈是 向低地址扩展 的数据结构

  • 栈是一块 连续的内存区域,遵循 先进后出(FILO) 原则

  • 栈区一般在 运行时 分配

存储

栈区是由 编译器自动分配并释放的,主要来存储

  • 局部变量

  • 函数的参数,例如函数的隐藏参数(id self, SEL _cmd)

优缺点

  • 优点:因为栈是由 编译器自动分配并释放 的,不会产生内存碎片,所以 快速高效

  • 确定:栈的内存大小有限制,数据不灵活

    • iOS主线程大小是1MB
    • 其他线程是512KB
    • MAC只有8MB

以上内存大小的说明,在Threading Programming Guide 中有相关说明,如下图:

堆区

*定义

  • 堆是 向高地址扩展 的数据结构

  • 堆是 不连续的内存区域,类似于 链表结构(便于增删,不便于查询),遵循 先进先出(FIFO) 原则

  • 堆的 地址空间 在iOS中是是动态的

  • 堆区的分配一般是以在 运行时分配

存储

堆区是 由程序员动态分配和释放 的,如果程序员不释放,程序结束后,可能由操作系统回收,主要用于存放:

  • OC 中使用 alloc 或者 new 开辟空间创建 对象

  • C 语言中使用 malloc、calloc、realloc 分配的空间,需要 free 释放

优缺点

  • 优点:灵活方便,数据适应面广泛

  • 缺点:需 手动管理、速度慢,容易产生内存碎片

当需要访问堆中数据时,一般需要 先通过对象读取到栈区的指针地址,然后通过 指针地址访问堆区

全局区(静态区,即.bss & .data)

全局区是 编译时分配 的内存空间,在程序运行过程中,此内存中的数据一直存在,程序结束后由系统释放,主要存:

  • 未初始化的全局变量和静态变量,即BSS区(.bss)

  • 已初始化的全局变量和静态变量,即DATA区(.data)

其中,全局变量 是指变量值可以在 运行时被动态修改,而 静态变量static 修饰的变量,包含 静态局部变量静态全局变量

常量区(即.rodata)

常量区是 编译时分配 的内存空间,在 程序结束后由系统释放,主要存放:

  • 已经使用了的,且没有指向的 字符串常量

字符串常量因为可能在程序中被多次使用,所以在程序运行之前就会提前分配内存

代码区(即.text)

代码区是 由编译时分配,主要用于存放 程序运行时的代码,代码会被编译成 二进制存进内存

内存五大区验证

运行下面的一段代码,看看变量在内存中是如何分配的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int a = 10; // 全局区(已初始化的全局变量)
char * b; // 全局区(未初始化的全局变量)
- (void)test{
NSInteger i = 123; // 栈区(局部变量)
NSLog(@"i的内存地址:%p", &i);

NSString *string = @"ZJ"; // 常量区(字符串常量)
NSLog(@"string的内存地址:%p", string);
NSLog(@"&string的内存地址:%p", &string);

NSObject *obj = [[NSObject alloc] init]; // 堆区(alloc对象)
NSLog(@"obj的内存地址:%p", obj);
NSLog(@"&obj的内存地址:%p", &obj);
}

运行结果如下:

1
2
3
4
5
2022-03-11 14:34:25.438913+0800 内存五大区[70321:4340509] i的内存地址:0x16f6f5a18
2022-03-11 14:34:25.438976+0800 内存五大区[70321:4340509] string的内存地址:0x100710098
2022-03-11 14:34:25.438997+0800 内存五大区[70321:4340509] &string的内存地址:0x16f6f5a10
2022-03-11 14:34:25.439014+0800 内存五大区[70321:4340509] obj的内存地址:0x280fa0bc0
2022-03-11 14:34:25.439031+0800 内存五大区[70321:4340509] &obj的内存地址:0x16f6f5a08
  • 对于 局部变量i, 存放在栈区

  • 对于 字符串对象string,分别打印了 string得对象地址string对象的指针地址

    • string的 对象地址 是是存放在 常量区
    • string 对象的指针地址,是存放在 栈区
  • 对于 alloc创建的对象obj,分别打印了 obj得对象地址obj对象的指针地址

    • obj的 对象地址 是存放在 堆区
    • obj 对象的指针地址 是存放在 栈区

函数栈

  • 函数栈 又称为 栈区,在内存中从高地址往低地址分配,与堆区相对,具体图示请看上面

  • 栈帧 是指 函数(运行中且未完成)占用的一块独立的连续内存区域

  • 应用中新创建的 每个线程都有专用的栈空间,栈可以在线程期间自由使用,而线程中有千千万万的函数调用,这些函数 共享 进程的这个 栈空间每个函数所使用的栈空间是一个栈帧,所有栈帧就组成了这个线程完成的栈

  • 函数调用是发生在栈上 的,每个 函数的相关信息(例如局部变量、调用记录等)都 存储在一个栈帧 中,每执行一次 函数调用,就会生成一个与其相关的栈帧,然后将其 栈帧压入函数栈,而当函数 执行结束,则将此函数对应的 栈帧出栈并释放掉

如下图所示,是经典图- ARM的栈帧布局方式

  • 其中 main stack frame 为调用函数的栈帧

  • func1 stack frame 为当前 当前函数(被调用者)的栈帧

  • 栈底 地址,栈向下增长

  • FP 就是 栈基址,它指向函数的 栈帧起始地址

  • SP 则是函数的 栈指针,它指向 栈顶 的位置

  • ARM压栈顺序 很是规则(也比较容易被黑客攻破),依次为 当前函数指针PC返回指针LR栈指针SP栈基址FP传入参数个数及指针本地变量临时变量。如果函数准备调用另一个函数,跳转之前临时变量区先要保存另一个函数的参数

  • ARM 也可以 用栈基址和栈指针明确标示栈帧的位置,栈指针SP一直移动,ARM的特点是,两个栈空间内的地址(SP+FP)前面,必然有两个代码地址(PC+LP)明确标示着调用函数位置内的某个地址

堆栈溢出

一般情况下应用程序是不需要考虑堆和栈的大小的,但是事实上堆和栈不是无上限的,过多的递归会导致栈溢出过多的alloc变量会导致堆溢出

所以 预防堆栈溢出 的方法:

  • 避免层次过深递归 调用

  • 不要使用过多的局部变量,控制局部变量的大小

  • 避免分配 占用空间 太大的对象,并 及时释放

  • 实在不行,适当的情景下 调用系统API修改线程的堆栈大小

栈帧示例

描述下面代码的栈帧变化

栈帧程序示例

1
2
3
4
5
6
7
8
9
10
11
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);
}

程序执行时,栈区中栈帧的变化如下图所示:

  • Post title:OC底层原理24:内存五大区
  • Post author:张建
  • Create time:2021-02-14 16:38:08
  • Post link:https://redefine.ohevan.com/2021/02/14/OC底层原理/OC底层原理24:内存五大区/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.