问题

 

目录

1:内存基础概念

2:内存五大区

3:函数栈

预备

 

正文

一、内存基础概念

1.1 物理内存 & 虚拟内存

  • 物理内存(Physical Memory):指通过物理内存条而获得的内存空间,和虚拟内存对应;主要作用是:设备运行时为操作系统和各种程序提供临时储存空间;iPhone 6 和 6 Plus 及之前都是 1G 内存、iPhone XS Max 和 11 Pro 是 4GB 内存,目前比较新的iPhone 12 Pro 是 6GB 内存;
  • 虚拟内存(Virtual Memory):是计算机系统内存管理的一种技术,为每一个进程提供了一个 一致的、私有的地址空间;其主要作用是:保护了每个进程的地址空间不会被其他进程破坏,降低内存管理的复杂性;32位设备虚拟内存大小是4GB,64位设备(5s以后的设备)是 4GB * 4GB;
  • 虚拟内存是进程运行时所有内存空间的总和,并且可能有一部分不在物理内存中;

1.2 段页式存储

  • 目前,大部分通用的计算机的内存管理使用 段页式存储结构;用户程序先分段,每个段内再分页;而 页是存储的最基本单位,iOS设备的 arm64 架构后,页大小是16KB;
  • 利用 逻辑地址(段号 + 段内页号 + 页内地址) 进行地址变化,获得物理地址;这样的话,在段页式结构中,须三次访问内存才能获取数据或指令;
  • 当进程访问一个虚拟内存的页时,而对应的物理内存却不存在时,会触发一次 Page Fault(缺页中断),将需要的数据 or 指令从磁盘加载到物理内存页中,建立映射关系,然后再恢复现场,程序本身是无感知的;

二:内存5大区

按照地址排列: 栈区 -> 堆区 -> 全局静态区 -> 常量区 -> 代码区 (内核区保留部分不再考虑范围内)

ios虚拟内存插件 ios6 虚拟内存_局部变量

ios虚拟内存插件 ios6 虚拟内存_ios虚拟内存插件_02

补充说明:

  1. 内存五大区,实际是指虚拟内存,而不是真实物理内存
  2. iOS系统中,应用虚拟内存默认分配4G大小,但五大区占3G,还有1G五大区之外的内核区

1:栈区(Stack)

1.1:栈区特点

  1. 函数内部定义的局部变量数组,都存放在栈区; (比如每个函数都有的(id self, SEL _cmd)
  2. 栈区的内存空间系统管理。(函数调用开辟空间,函数调用结束回收空间)
  3. 栈是从高地址低地址扩展,是一块连续的内存区域,遵循FILO先进后出原则,效率高。
  4. 栈区一般在运行时进行分配
  5. 栈的地址空间在iOS中通常以0x7开头 

栈区空间大小较小,所以空间比较宝贵,但是读取写入效率高,下面我们看看栈区都会存储什么内容

1.2:存储

栈区一般是由编译器来自动分配和释放的,主要用来存储一下内容

  • 局部变量
  • 函数的参数,例如函数的隐藏参数(id self,SEL_cmd)

1.3:优缺点

  • 优点
  • 不会产生内存碎片(回收释放有系统自己控制)
  • 高效的读写速度
  • 缺点
  • 栈的内存较小(iOS主线程栈大小1MB,其它线程512KB)
  • 存储数据不灵活(存储内容基本固定,由编译器分配)
1.4:缓冲区域

栈区堆区中间有小块未使用内存区域。用于给栈区堆区之间创建一个缓冲区域

  • 溢出:
    到达缓冲区数据向小缓冲区复制的过程中,由于没有注意小缓冲区的边界,导致小缓存区满了,从而覆盖了和小缓存区相邻内存区域的其他数据引起内存问题
    (就像桶盛水,水多了,自然越界溢出来了。)

2:堆区 

2.1:定义

  • 1.堆是从低向高地址扩展的数据结构
  • 2.堆是不连续的内存区域,类似于链表结构,遵循先进先出原则:FIFO
  • 3.堆的地址空间在iOS中通常以0x6开头
  • 4.堆区的分配一般也是在运行时进行分配

2.2:存储

堆区一般是由开发者自己分配和释放的,同时系统也会在必要的时候对堆区存储的内容进行回收和释放(系统检测属性或者对象引用计数为零时,进行回收

  • OC使用alloc、new、block或者使用copy创建的对象都会存在这里(ARC下编译器会自动在合适的时候释放内存,而在MRC下需要开发者手动释放)
  • C语言中使用malloc、calloc、realloc分配的空间(需要free释放)

2.3:优缺点

  • 优点
  • 使用灵活方便,数据使用更加广泛
  • 缺点
  • 内存需要手动管理
  • 容易产生碎片
  • 读取速度和栈区比较慢

访问堆区内存时,一般是先通过对象读取到对象所在的栈区的指针地址,然后通过指针地址访问堆区

需要注意:

  • 野指针:提前释放了,查询时找不到内容
  • 内存泄露 :没有释放,一直占用内存
  • 过度释放:对已释放的对象进行release操作。

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

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

  • 未初始化全局变量静态变量,即BSS区(.bss)
  • 已初始化全局变量静态变量,即数据区(.data)

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

static修饰的变量仅执行一次生命周期整个程序运行期

4:常量区(即.rodata)

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

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

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

存放常量(整型字符型浮点字符串等),整个程序运行期不能被改变。

空间由系统管理,生命周期整个程序运行期

5:代码区(即.text)

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

存放程序执行CPU指令。(编译期代码转换为CPU指令)

defineconst区别:

define: 宏。编译期不会进行语法识别没有类型。编译期会分配内存每次使用都会进行宏替换开辟内存

const: 常量。编译期进行语法识别,需要指定类型。编译期分配内存,仅在第一次使用时,开辟内存记录内存地址后续调用时开辟内存,直接返回记录的内存地址效率内存占用更

6:内存5大区代码验证

可以通过以下代码,加深印象:

- (void)test {
    
    NSInteger i = 666;
    NSLog(@"NSInteger i -> 内存地址:%p", &i); // 【局部变量】 栈区

    NSString * name = @"HT";
    NSLog(@"NSString name -> 内存地址: %p", name); // 【字符串内容】 存放在常量区
    NSLog(@"NSString name -> 指针地址: %p", &name);// 【局部变量name的指针】 存放在栈区
    
    NSObject * objc = [NSObject new];
    NSLog(@"NSObject objc -> 内存地址: %p", objc);// 【对象的内容】 存放在堆区
    NSLog(@"NSObject objc -> 指针地址: %p", &objc);//【对象的指针】 存放在栈区
}

打印结果: (0x7开头: 栈区 、 0x1开头: 常量区、 0x6开头: 堆区

 

ios虚拟内存插件 ios6 虚拟内存_堆区_03

 

  • 对于局部变量i,从地址可以看出是0x7开头,所以i存放在栈区
  • 对于字符串对象string,分别打印了string的对象地址和 string对象的指针地址
  • string的对象地址是以0x1开头,说明是存放在常量区
  • string对象的指针地址是以0x7开头,说明是存放在栈区
  • 对于alloc创建的对象obj,分别打印了obj的对象地址和 obj对象的指针地址(可以参考前文的汇总图)
  • obj的对象地址是以0x6开头,说明是存放在堆区
  • obj对象的指针地址是以0x7开头,说明是存放在栈区

三:函数栈

函数栈又称为栈区,在内存中从高地址往低地址分配,与堆区相对,具体图示请查看文章最开始的图示

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

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

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

  • 其中main stack frame调用函数的栈帧
  • func1 stack frame当前函数(被调用者)的栈帧
  • 栈底地址,栈向下增长。
  • FP就是栈基址,它指向函数的栈帧起始地址
  • SP则是函数的栈指针,它指向栈顶的位置。
  • ARM压栈顺序很是规矩(也比较容易被黑客攻破么),依次为当前函数指针PC返回指针LR栈指针SP栈基址FP传入参数个数及指针本地变量临时变量。如果函数准备调用另一个函数,跳转之前临时变量区先要保存另一个函数的参数。
  • ARM也可以用栈基址和栈指针明确标示栈帧的位置,栈指针SP一直移动,ARM的特点是,两个栈空间内的地址(SP+FP)前面,必然有两个代码地址(PC+LR)明确标示着调用函数位置内的某个地址

堆栈溢出

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

所以预防堆栈溢出的方法:
(1)避免层次过深递归调用;

(2)不要使用过多的局部变量,控制局部变量的大小;

(3)避免分配占用空间太大的对象,并及时释放;

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

栈帧示例

描述下面代码的栈帧变化

栈帧程序示例

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

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

ios虚拟内存插件 ios6 虚拟内存_堆区_04

 四:内存如何分配

1:栈区地址如何分配?

- (void)testStack{
    NSLog(@"************栈区************");
    // 栈区
    int a = 10;
    int b = 20;
    NSObject *object = [NSObject new];
    NSLog(@"a == \t%p",&a);
    NSLog(@"b == \t%p",&b);
    NSLog(@"object == \t%p",&object);
}

ios虚拟内存插件 ios6 虚拟内存_ios虚拟内存插件_05

由上图的打印结果可以看出:

  • 局部变量的地址在栈区
  • 栈区的地址分配时,是高地址 -> 低地址

2:堆区地址如何分配?

- (void)testHeap{
    NSLog(@"************堆区************");
    // 堆区
    NSObject *object1 = [NSObject new];
    NSObject *object2 = [NSObject new];
    NSObject *object3 = [NSObject new];
    NSObject *object4 = [NSObject new];
    NSObject *object5 = [NSObject new];
    NSObject *object6 = [NSObject new];
    NSObject *object7 = [NSObject new];
    NSObject *object8 = [NSObject new];
    NSObject *object9 = [NSObject new];
    NSLog(@"object1 = %@",object1);
    NSLog(@"object2 = %@",object2);
    NSLog(@"object3 = %@",object3);
    NSLog(@"object4 = %@",object4);
    NSLog(@"object5 = %@",object5);
    NSLog(@"object6 = %@",object6);
    NSLog(@"object7 = %@",object7);
    NSLog(@"object8 = %@",object8);
    NSLog(@"object9 = %@",object9);
    // 访问---通过对象->堆区地址->存在栈区的指针
}

ios虚拟内存插件 ios6 虚拟内存_堆区_06

由上图的打印结果可以看出:

  • 局部变量的地址在栈区,对象的地址在堆区
  • 栈区的地址分配时,是低地址 -> 高地址

3:BSS段地址如何分配?

int clA;
int clB = 10;

static int bssA;
static NSString *bssStr1;

static int bssB = 10;
static NSString *bssStr2 = @"bss";

- (void)testConst{
    
    NSLog(@"************BSS段************");
    NSLog(@"clA == \t%p",&clA);
    NSLog(@"bssA == \t%p",&bssA);
    NSLog(@"bssStr1 == \t%p",&bssStr1);
    
    NSLog(@"***********DATA段************");
    NSLog(@"clB == \t%p",&clB);
    NSLog(@"bssB == \t%p",&bssB);
    NSLog(@"bssStr2 == \t%p",&bssStr2);
}

ios虚拟内存插件 ios6 虚拟内存_堆区_07

 

由上图的打印结果可以看出:

  • 未初始化话的全局变量和静态变量,在BSS段
  • BSS段的地址分配时,是低地址 -> 高地址
  • 已初始化话的全局变量和静态变量,在DATA段
  • DATA段的地址分配,与变量定义的顺序无关

4:静态区安全测试

静态变量的作用范围是当前文件内

  • 当前文件更改静态变量后,本文件内再访问,是更改后的值,但不影响别的文件中的这个静态变量的值
  • 别的文件引入静态变量后,拿到的是静态变量的初始值,修改后再访问是自己修改后的值。
  • 也就相当于引用别的文件时,底层会深拷贝一份静态变量放在了自己的文件中,以后访问及操作的都是本文件内的这个变量,对别的文件没有影响。

注意