15 - iOS的内存认识

OC底层原理探索文档汇总

主要内容:

1、内存的认识
2、栈区和堆区的使用验证
3、内存泄漏和内存溢出

内存的认识

我们所说的内存其实准确的说是虚拟内存,不是物理内存,由多张页组成。
分成内核区和数据区,其中数据区包括五大区以及保留区。

  • 内核区是系统进行内核处理操作的区域
  • 五大区就是我们常说的栈、堆、全局区、常量区、代码区。
  • 保留区是预留给系统处理nil等的区域

以4G手机为例,1个G用来存放内核区,3个G用来存放数据区。

我们所涉及的其实就是五大区,通常使用的是栈和堆。这两个要着重了解。

内存布局.jpg

五大区的认识

内存五大区图示.png

介绍: 栈是从高地址向低地址扩展的一段连续内存空间,遵循先进后出原则

存储:

  • 用来存放局部变量(函数参数)
  • 系统会自动分配和释放,随着方法的创建而创建,方法的销毁而销毁
  • 一般在运行时分配
  • 在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个字节
栈的认识.png

堆区的使用验证

代码:

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

图示:

引用类型的指针偏移.png

堆栈溢出的认识

溢出原因

一般情况下应用程序是不需要考虑堆和栈的大小的,但是事实上堆和栈都不是无上限的。当我们写入的数据超出了分配的内存空间,就会造成溢出。

  • 过多的递归会导致栈溢出。
  • 过多的alloc变量会导致堆溢出。

预防方法

栈:

  • 避免层次过深的递归调用;
  • 一个函数内不要使用过多的局部变量,控制局部变量的大小

堆:

  • 避免分配占用空间太大的对象
  • 对象要及时释放
  • 实在不行,适当的情景下调用系统API修改线程的堆栈大小,可以改大;

堆栈泄漏的认识

内存泄漏通俗来说:本该回收的对象没有及时回收,无法分配给别人,相当于失去了这一部分内存。

原因可能有这几种:

  • 创建大量对象后没有去释放
  • 循环引用,对象无法释放

总结

  • 我们的内存分为内核区和数据区,数据区有五大区和保留区
  • 我们通常所说的内存管理其实就是指管理堆,因为栈会很快被释放掉,全局区、常量池、代码区都是系统控制的
  • 全局区是可读可写的,编译时分配好内存空间后还可以动态的写入,
  • 常量区和方法区都是只读的,在编译时就已经分配好后不可再动态写入
  • 避免堆栈溢出

你可能感兴趣的:(15 - iOS的内存认识)