文章目录
前言
iOS里内存主要分为五大区域:栈区、堆区、全局区/静态区、常量区、代码区
五大分区描述的是进程虚拟地址空间的布局,并不是物理内存布局。真正访问时,由MMU完成虚拟地址到物理地址的映射
具体如图:

从下到上分别是从低地址到高地址,代码区→常量区→全局 / 静态区→堆区→栈区,内核区位于最高地址
- 堆区向上生长,栈区向下生长,这里为啥这么设计是因为这样相向设计,只有到几乎用满整个内存的时候才会相撞,但是同方向几乎很快,一增长就冲突,这样相向提高了内存利用率还可以延迟冲突发生时间
栈为什么一定向下增长?
栈向低地址增长主要是因为早期 CPU 的设计使栈指针递减更高效,同时配合内存高地址布局可以与堆形成相向增长,从而最大化利用内存空间。
为什么堆向上增长?malloc不断申请更高地址
一、栈区(Stack)
核心特性
- 存储方向:从高地址向低地址分配,内存连续
- 管理方式:系统自动分配/释放(函数结束后立即回收)
- 典型存储:局部变量、方法参数(如self,_cmd)、函数返回地址
- 地址特征:iOS中以0x7或0x16开头
代码演示:
objc
int a = 10;
NSLog(@"a == %p size == %lu", &a, sizeof(a));
NSLog(@"方法参数self:%p", &self);
NSLog(@"方法参数cmd:%p", &_cmd);
运行结果:

地址分别从0x16f0a3988(self)->0x16f0a3980(cmd)->0x16f0a396c(a)由高到低递减,步长8字节,验证栈的后进先出的特性
优缺点
- 优点:速度快(系统自动管理),无内存碎片
- 缺点:空间受限(iOS主线程栈大小1MB,其他线程512KB)
函数帧和栈帧
栈帧
函数运行时在栈区分配的独立连续的内存区域,包含:
- 局部变量:函数内定义的变量(如
int z = x + y😉 - 调用记录:返回地址(LR)、当前函数指针(PC)、栈基址(FP)、栈指针(SP)等
- 参数传递:函数参数通过栈帧传递(如
Add(a, b)中的a和b)
函数调用流程
-
压栈:调用函数时,系统重新创建新的栈帧,将参数、局部变量、PC/LR等压入栈区
例:main函数调用Add(a, b)时,先压入a、b,再压入Add函数的栈帧
-
执行:函数在栈帧内执行,操作局部变量(计算
z = x + y) -
出栈:函数执行完毕,栈帧出栈,释放内存,程序通过LR返回调用处(从Add返回main)
objc
- (void)testStackFrame {
int a = 10;
int b = 20;
int sum = [self addWithA:a b:b];
NSLog(@"sum = %d", sum);
}
- (int)addWithA:(int)a b:(int)b {
return a + b;
}
栈帧变化
testStackFrame调用时,栈帧压入a、b、sum等局部变量。- 调用
addWithA:b:时,新栈帧压入参数a、b,计算后通过 LR 返回结果。 - 函数结束后,两层栈帧依次出栈,释放内存
二、堆区(Heap)
核心特性
- 存储方向:从低地址向高地址分配,内存不连续(基于链表管理)
- 管理方式:手动分配/释放(
alloc/free等)要避免内存泄漏 - 典型存储:OC 对象(
[NSObject new])、动态分配的数据(malloc) - 地址特征:iOS里以0x6开头
代码演示:
objc
NSObject *object1 = [NSObject new];
NSObject *object2 = [NSObject new];
NSLog(@"object1 = %@", object1);
NSLog(@"object2 = %@", object2);

我们看到这两地址不是连续的,验证堆非连续分配的特性
对象在堆里,那成员变量在哪里
objc
@interface Person : NSObject
{
int age;
}
@end
Person *p = [Person new];
如上代码的话
p(指针变量)在栈(如果是局部变量)。Person对象本体在堆。age是对象的一部分,也在堆里,跟着对象一起存储,而不是单独在栈或全局区。
和栈区的对比
| 纬度 | 栈区 | 堆区 |
|---|---|---|
| 分配方式 | 系统自动(LIFO) | 手动分配(链表遍历) |
| 内存的连续性 | 连续 | 不连续 |
| 生命周期 | 函数作用域内 | 手动释放或程序结束 |
| 空间大小 | 受限制(1MB/512KB) | 受限于虚拟内存 |
| 碎片问题 | 无 | 容易产生 |
堆栈溢出的风险
- 栈溢出
- 原因:递归深度过深(如无限递归)或者局部变量过多,超出栈空间限制(主线程1MB)
- 后果:程序崩溃,抛出
EXC_BAD_ACCESS异常
- 堆溢出
- 原因:频繁分配大内存(如循环创建未释放的对象),耗尽虚拟内存
- 后果:内存不足,系统终止进程
堆内存碎片为什么产生
假设先 alloc 一个 100 字节的对象 A,又 alloc 一个 200 字节的对象 B,然后 A 释放了。此时堆里有一块 100 字节的空洞。下次要 alloc 一个 150 字节的对象,这 100 字节空洞用不了,只能跳到别处分配 → 碎片越来越多。
这里如果后面有小于等于100的还可以放进100内存,只是后面如果比100大,就会一直空出来,后面可能会越空越多
三、全局区/静态区(Global/Static)
内存划分
- .bss段:存储的是未初始化的全局变量和静态变量,程序启动时自动清0
- .data段:存储已初始化的全局变量和静态变量
代码演示:
objc
int globalVar; // 未初始化全局变量(.bss)
static int staticVar; // 未初始化静态变量(.bss)
int initGlobal = 200; // 已初始化全局变量(.data)
static int initStatic = 211; // 已初始化静态变量(.data)
- (void)test{
NSLog(@"globalVar = %p", &globalVar);
NSLog(@"staticVar = %p", &staticVar);
NSLog(@"initGlobal = %p", &initGlobal);
NSLog(@"initStatic = %p", &initStatic);
}

我们看到.bss段和.data地址分别都是连续的,都是相差4字节
static静态变量
objc
void test() {
static int count = 0;//在全局/静态区,但是只在函数内可见
count++;
NSLog(@"%d",count);
}
//多次调用test,count地址不变,值增加
区别
| 类型 | 作用域 | 初始化时机 |
|---|---|---|
| 全局变量 | 全程序可见 | 程序启动时分配 |
| 静态全局变量 | 当前文件可见 | 程序启动时分配 |
| 静态局部变量 | 函数内可见 | 第一次调用时分配 |
四、常量区(Constant)
- 存储内容:字符串常量(如
@"Hello")以及只读数据、const修饰的只读数据(编译器决定) - 特性
- 只读,程序运行时不可以修改
- 编译时分配,程序结束后由系统释放
五、代码区(Code/Text)
- 存储内容:编译后的二进制的指令(函数题、方法实现)
- 特点:
- 只读,防止程序运行时意外改变代码
- 共享性:相同程序的多个实例共享代码区(多个app的进程共享系统库代码)
- 安全性:防止恶意代码注入和篡改,防御缓冲区的溢出攻击
代码区对应Mach-O中的TEXT段,主要包括:_ text 、_cstring等只读内容。
总结
| 区域 | 存什么 | 谁管理 | 是否连续 | 生命周期 |
|---|---|---|---|---|
| 栈 | 局部变量、参数 | 系统 | 连续 | 函数结束 |
| 堆 | OC对象、malloc | ARC/MRC、程序员 | 不连续 | 引用计数 |
| 全局/静态 | 全局变量、static | 系统 | 连续 | 程序结束 |
| 常量 | 字符串常量、只读数据 | 系统 | 只读 | 程序结束 |
| 代码 | 函数、方法实现 | 系统 | 只读 | 程序结束 |