01-主题|内存管理@iOS-内存五大分区

7 阅读8分钟

本文介绍 iOS 进程内存中的五大分区:栈区、堆区、全局区(静态区)、常量区、代码区。涵盖各区的定义、存储内容、地址特征、分配与释放时机,以及栈帧与堆栈溢出等要点,并给出验证示例与对比小结。


目录


一、总览

在 iOS 中,进程内存主要分为 栈区、堆区、全局区、常量区、代码区 五大区域。各区的地址范围、分配时机与管理者不同,对应不同的使用场景与注意事项。

1.1 五大区速览表

分区常见地址前缀(iOS)分配时机分配/释放者主要存放
栈区0x7...运行时编译器自动分配与释放局部变量、函数参数、返回地址等
堆区0x6...运行时程序员分配(ARC/MRC 管理释放)alloc/new 对象、malloc 等动态分配
全局区0x1...编译时程序结束后系统释放.bss:未初始化全局/静态变量;.data:已初始化全局/静态变量
常量区0x1...(只读)编译时程序结束后系统释放字符串常量等只读数据(.rodata)
代码区编译时程序结束后系统释放程序机器码(.text)

说明:地址前缀因架构与系统版本可能略有差异,实际以运行打印为准;全局区与常量区在部分环境下可能同属 0x1 段,通过只读/可写与段名区分。

1.2 内存布局示意(由高地址到低地址)

flowchart TB
    subgraph 高地址
        A[栈区 Stack<br/>0x7... 向下增长]
    end
    subgraph 中间
        B[堆区 Heap<br/>0x6... 向上增长]
    end
    subgraph 低地址
        C[全局区 .bss / .data<br/>0x1...]
        D[常量区 .rodata]
        E[代码区 .text]
    end
    A --> B
    B --> C
    C --> D
    D --> E

二、栈区(Stack)

2.1 定义

  • 栈是系统级数据结构,与进程/线程一一对应(每个线程有独立栈空间)。
  • 栈是向低地址扩展的连续内存区域,遵循先进后出(FILO)
  • 在 iOS 中栈地址通常以 0x7 开头。
  • 栈空间在运行时由系统分配。

2.2 存储内容

栈区由编译器自动分配并释放,主要存放:

  • 局部变量(含基本类型、指针变量等)。
  • 函数参数,包括隐藏参数(如 Objective-C 的 id selfSEL _cmd)。
  • 函数调用的返回地址、栈帧信息等(详见 八、函数栈与栈帧)。

2.3 特点与约束

项目说明
优点自动分配释放,无内存碎片,访问高效
缺点容量有限,数据生命周期与作用域绑定,不够灵活
栈大小主线程栈一般为 1MB,子线程一般为 512KB;macOS 上主线程栈约 8MB。详见 Threading Programming Guide

三、堆区(Heap)

3.1 定义

  • 堆是向高地址扩展不连续内存区域,内部常以链表等结构管理空闲块。
  • 在 iOS 中堆地址通常以 0x6 开头。
  • 堆空间在运行时由程序动态申请,大小灵活。

3.2 存储内容

堆区由程序员(或 ARC)分配与释放,主要存放:

  • Objective-Callocnewcopy 等创建的对象
  • C 语言malloccallocrealloc 分配的内存,需配套 free 释放。

3.3 访问方式

访问堆上对象时,通常先通过栈上的指针(或寄存器)得到对象地址,再通过该地址访问堆内存。即:栈存「指针」,堆存「实际对象数据」。

3.4 特点

项目说明
优点容量大、生命周期可灵活控制,适应面广。
缺点需管理释放(MRC 手动 / ARC 自动),分配与释放比栈慢,易产生内存碎片

四、全局区(静态区:.bss 与 .data)

4.1 定义

  • 全局区在编译时即确定布局,在 iOS 中地址常以 0x1 开头。
  • 程序运行期间该区域一直存在,程序结束后由系统释放

4.2 存储内容

内容
.bss未初始化的全局变量、静态变量(系统会将其初始化为 0)。
.data已初始化的全局变量、静态变量。

全局变量静态变量:全局变量在全局可见;静态变量static 修饰,包括静态局部变量(作用域限于函数内、生命周期贯穿程序)和静态全局变量(本文件内可见)。


五、常量区(.rodata)

5.1 定义

  • 常量区在编译时分配,只读,程序结束后由系统释放。
  • 对应段名常为 .rodata(read-only data)。

5.2 存储内容

  • 字符串常量(如 @"hello"):在程序运行前即分配好,只读,可被多处引用。
  • 其他只读常量(如 const 全局、字面量等,视实现而定)。

已在程序中使用的、且无其他指针指向的字符串常量,会在运行前就分配在常量区,以便复用与节省空间。


六、代码区(.text)

6.1 定义与内容

  • 代码区编译时分配,主要存放程序的机器码(指令),对应段一般为 .text
  • 代码被编译成二进制后加载进内存,通常为只读,程序结束后由系统释放。

七、五大区对比与验证

7.1 对比小结表

维度栈区堆区全局区常量区代码区
地址特征0x7,向下增长0x6,向上增长0x10x1(只读)低地址
分配时机运行时运行时编译时编译时编译时
谁释放编译器/系统程序员/ARC系统系统系统
典型内容局部变量、参数对象、malloc全局/静态变量字符串常量等机器码

7.2 验证代码示例

通过打印变量与指针的地址,可直观看到各区的分布:

- (void)test {
    NSInteger i = 123;
    NSLog(@"i 的地址(栈上局部变量): %p", &i);           // 预期 0x7...

    NSString *string = @"CJL";
    NSLog(@"string 对象地址(常量区): %p", string);      // 预期 0x1...
    NSLog(@"&string 指针地址(栈): %p", &string);       // 预期 0x7...

    NSObject *obj = [[NSObject alloc] init];
    NSLog(@"obj 对象地址(堆): %p", obj);               // 预期 0x6...
    NSLog(@"&obj 指针地址(栈): %p", &obj);            // 预期 0x7...
}

7.3 结果解读

打印项含义预期分区
&i局部变量 i 的地址栈区(0x7...)
string字符串对象首地址常量区(0x1...)
&string指针变量 string 的地址栈区(0x7...)
objalloc 创建的对象首地址堆区(0x6...)
&obj指针变量 obj 的地址栈区(0x7...)

结论:指针变量本身在栈(或寄存器),对象/常量数据在堆或常量区;通过栈上的指针访问堆或常量区。


八、函数栈与栈帧

8.1 概念

  • 函数栈即线程使用的栈区,从高地址向低地址增长,与堆区相对。
  • 栈帧(Stack Frame)一次未完成调用的函数所占用的一块连续栈空间,用于存放该次调用的参数、局部变量、返回地址等。

8.2 线程与栈帧关系

  • 每个线程拥有独立的栈;该线程内所有函数调用共享这份栈空间。
  • 每发生一次函数调用,就生成一个栈帧并压栈;函数返回时,对应栈帧出栈并释放。
  • 当前线程的栈 = 当前所有未返回函数的栈帧组成的序列。

8.3 ARM 栈帧布局要点

  • 栈底在高地址,栈向低地址增长。
  • FP(Frame Pointer):栈基址,指向当前函数栈帧的起始位置。
  • SP(Stack Pointer):栈顶指针,随压栈/出栈移动。
  • 压栈顺序(典型):返回地址、保存的 FP、局部变量、临时变量等;若再调用子函数,会先为子函数参数预留空间。

8.4 栈帧变化示例

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);
    return 0;
}

执行过程简述:

  1. main 入栈:为 ab 等分配栈帧,调用 Add 前将参数压栈。
  2. Add 入栈:为 xyz 等分配新栈帧,SP 下移。
  3. Add 返回:Add 的栈帧出栈,SP 回到 main 的栈帧,返回值通过约定(寄存器或栈)传回 main。

九、堆栈溢出与预防

9.1 原因简述

  • 栈溢出:栈空间有限,递归过深局部变量过大会导致栈溢出。
  • 堆溢出:堆虽大但非无限;过多或过大的动态分配且不释放,会导致堆空间耗尽或 OOM。

9.2 预防要点

类型建议
避免层次过深的递归;控制局部变量数量和大小;必要时可考虑改为迭代或增大线程栈(系统 API,谨慎使用)。
避免分配过大的单块对象;及时释放不再使用的对象;注意循环引用与泄漏(ARC 下也要避免强引用环)。

延伸阅读


参考文献