OC底层原理探索文档汇总
主要内容:
1、内存的认识
2、栈区和堆区的使用验证
3、内存泄漏和内存溢出
内存的认识
我们所说的内存其实准确的说是虚拟内存,不是物理内存,由多张页组成。
分成内核区和数据区,其中数据区包括五大区以及保留区。
- 内核区是系统进行内核处理操作的区域
- 五大区就是我们常说的栈、堆、全局区、常量区、代码区。
- 保留区是预留给系统处理nil等的区域
以4G手机为例,1个G用来存放内核区,3个G用来存放数据区。
我们所涉及的其实就是五大区,通常使用的是栈和堆。这两个要着重了解。
五大区的认识
栈
介绍: 栈是从高地址向低地址扩展的一段连续内存空间,遵循先进后出原则
存储:
- 用来存放局部变量(函数参数)
- 系统会自动分配和释放,随着方法的创建而创建,方法的销毁而销毁
- 一般在运行时分配
- 在iOS中是以0X7开头的内存空间
优缺点:
优点: 栈是由编译器自动分配并释放的,不会产生内存碎片,所以快速高效
缺点: 内存大小有限制,数据不灵活
注意:
- 栈是系统数据结构,对应的线程或进程是唯一的,也就是一个栈不能被多个线程或进程共享
- 栈是从高地址向低地址扩展,也就是后添加的是低地址,先添加的在高地址,也就是后添加的是低地址,先添加的在高地址
栈为什么需要由高地址向低地址扩展?
- 这样设计可以使得堆和栈能够充分利用空闲的地址空间,因为栈和堆的空间大小会随着运行的过程不断变化,所以无法界定栈和堆的分割线。无法确定给堆分配多少空间。
- 所以栈和堆就分别从两种向中间扩展内存。可以最大限度的利用剩余的地址空间
堆
介绍:
- 堆是从低地址向高地址扩展的不连续的内存区域,通过指针来查找数据,遵循先进先出原则
- 分配堆的空间大小不确定,而且它的生命周期时间不确定,所以需要人为管理。
- 内存管理基本上可以说是管理堆,在iOS中是通过引用计数来管理的
存储:
- 堆区是由程序员的动态分配和释放的,堆区的分配一般是在运行时分配
- 用来存储引用类型数据
- 用于存放OC中alloc或者new开辟空间创造的对象
- 用于存放C语言中用malloc/calloc/realloc分配的空间,需要free释放
- 地址空间以0X6开头,空间的分配总是动态的
优缺点:
优点: 灵活方便,数据适应面广泛,容量大
缺点: 需手动管理,速度慢、容易产生内存碎片
全局区
介绍: 全局区是编译时分配的内存空间,用来存储全局变量和静态变量,包括BSS区和data区
存储:
BSS区:
- 存放未初始化的全局变量和静态变量
- 初始化后回收
data区:
- 存放已初始化的全局变量和静态变量
- 程序结束时系统回收
常量区
介绍: 常量区存储常量
注意:
- 常量区是编译时分配的内存空间
- 程序结束后由系统释放
代码区
介绍:
- 代码区是编译时分配主要用于存放程序运行时的代码,代码会被编译成二进制存进内存的
- 也就是已经被加载的类、常量,被编译后的二进制代码
存储区的验证
int quanju;
- (void)test{
//栈
NSInteger i = 123;
NSLog(@"i的内存地址:%p", &i);
//堆
NSObject *obj = [[NSObject alloc] init];
NSLog(@"obj的内存地址:%p", obj);
NSLog(@"&obj的内存地址:%p", &obj);
//静态
static NSInteger jingtai = 111;
NSLog(@"jingtai的内存地址:%p", &jingtai);
//全局区
quanju = 222;
NSLog(@"quanju的内存地址:%p", &quanju);
//常量池
NSString *string = @"CJL";
NSLog(@"string的内存地址:%p", string);
NSLog(@"&string的内存地址:%p", &string);
}
运行结果:
2021-11-09 22:23:24.958924+0800 内存管理[76267:847507] i的内存地址:0x7ff7b233ef18
2021-11-09 22:23:24.959026+0800 内存管理[76267:847507] obj的内存地址:0x600001b9c3b0
2021-11-09 22:23:24.959099+0800 内存管理[76267:847507] &obj的内存地址:0x7ff7b233ef10
2021-11-09 22:23:24.959180+0800 内存管理[76267:847507] jingtai的内存地址:0x10dbc6520
2021-11-09 22:23:24.959249+0800 内存管理[76267:847507] quanju的内存地址:0x10dbc66a8
2021-11-09 22:23:24.959315+0800 内存管理[76267:847507] string的内存地址:0x10dbc10c0
2021-11-09 22:23:24.959377+0800 内存管理[76267:847507] &string的内存地址:0x7ff7b233ef08
说明:
- 从中可以看出在栈中的数据是以0x7开头的
- 堆的数据是以0x6开头的
- 常量区、静态区、全局区的数据是以0x10开头的
栈区的使用验证
代码:
void baseDataTypeTest(){
int a = 9;
int b = 10;
int *f = &a;
NSLog(@"基本数据类型的变量a:%d -- 变量地址:%p",a,&a);
NSLog(@"基本数据类型的变量b:%d -- 变量地址:%p",b,&b);
NSLog(@"基本数据类型的指针a:%p -- 指针b:%p",f,f-1);
NSLog(@"指针变量f所指向的a变量的内容:%d -- 变量b的内容:%d",*f,*(f-1));
NSLog(@"指针变量f所在的内存地址:%p",&f);
}
结果:
2021-10-12 12:29:14.880387+0800 指针偏移[70684:2791244] 基本数据类型的变量a:9 -- 变量地址:0x1040bb2bc基本类型都是在
2021-10-12 12:29:14.880773+0800 指针偏移[70684:2791244] 基本数据类型的变量b:10 -- 变量地址:0x1040bb2b8
2021-10-12 12:29:14.880818+0800 指针偏移[70684:2791244] 基本数据类型的指针a:0x1040bb2bc -- 指针b:0x1040bb2b8
2021-10-12 12:29:14.880848+0800 指针偏移[70684:2791244] 指针变量f所指向的a变量的内容:9 -- 变量b的内容:10
2021-10-12 12:29:14.880872+0800 指针偏移[70684:2791244] 指针变量f所在的内存地址:0x1040bb2b0
分析:
- 1、基本数据类型存储在栈中,因此直接将10数值存储在栈中
- 2、栈的存储方式是从高地址到低地址,所以变量a的地址值比变量b的地址值要高
- 3、因为类型为int型,a占有4个字节,所以b地址比a地址低了4个字节
- 4、&a是将变量a的地址取出来
- 5、int *f = &a是将变量a的地址赋值到指针变量e上
- 6、打印f和f-1可以看到分别打印的是a和b的地址,也可以证明指针变量存储的就是地址
- 7、为什么是f-1,而不是f+1,这是因为栈的存储方式是从高地址到低地址,所以得到b的指针需要通过f-1
- 8、*f是对指针变量使用 *,可以得到指针变量存储的地址所存储的内容,因此 * f, *(f-1)分别到的a和b变量的内容
- 9、也可以对指针变量f取地址,取到的地址就是指针变量的地址,被b地址小了8个字节
堆区的使用验证
代码:
void quoteTest(){
NSperson *person1 = [NSperson alloc];
NSperson *person2 = [NSperson alloc];
NSLog(@"引用类型的指针person1:%@ -- 变量地址:%p",person1,&person1);
NSLog(@"引用类型的指针person2:%@ -- 变量地址:%p",person2,&person2);
}
运行结果:
2021-10-12 12:29:14.881025+0800 指针偏移[70684:2791244] 引用类型的指针person1: -- 变量地址:0x1040bb2b8
2021-10-12 12:29:14.881069+0800 指针偏移[70684:2791244] 引用类型的指针person2: -- 变量地址:0x1040bb2b0
分析:
- 指针是在栈中,对象是在堆中
- 一个指针占8个字节,所以指针按地址空间来说&p1和&p2二者之间相差8个字节
- 而指针本身是紧挨着的,也就是说&p1的下一个指针就是&p2
图示:
堆栈溢出的认识
溢出原因
一般情况下应用程序是不需要考虑堆和栈的大小的,但是事实上堆和栈都不是无上限的。当我们写入的数据超出了分配的内存空间,就会造成溢出。
- 过多的递归会导致栈溢出。
- 过多的alloc变量会导致堆溢出。
预防方法
栈:
- 避免层次过深的递归调用;
- 一个函数内不要使用过多的局部变量,控制局部变量的大小
堆:
- 避免分配占用空间太大的对象
- 对象要及时释放
- 实在不行,适当的情景下调用系统API修改线程的堆栈大小,可以改大;
堆栈泄漏的认识
内存泄漏通俗来说:本该回收的对象没有及时回收,无法分配给别人,相当于失去了这一部分内存。
原因可能有这几种:
- 创建大量对象后没有去释放
- 循环引用,对象无法释放
总结
- 我们的内存分为内核区和数据区,数据区有五大区和保留区
- 我们通常所说的内存管理其实就是指管理堆,因为栈会很快被释放掉,全局区、常量池、代码区都是系统控制的
- 全局区是可读可写的,编译时分配好内存空间后还可以动态的写入,
- 常量区和方法区都是只读的,在编译时就已经分配好后不可再动态写入
- 避免堆栈溢出