内存区域划分

认清内存区域到底怎么划分

Posted by Renchao on July 10, 2017

内存区域划分

[TOC]

内存区域

内存区域可以划分为堆、栈、全局\静态区、常量区、代码区。下表说明了内存区域的划分情况:

五大区域 分配和释放 存放对象 特点
编译器自动分配释放 局部变量 实参 先进后出 512k 连续 从大到小 速度快 系统管理 有限制 不灵活
程序员手动释放 用new创建的对象 被创建对象的所有成员变量 先进先出 所有程序共享 存储大数据 手动管理 不连续 速度没有栈区快
全局区/静态区 程序结束后由系统释放 没有初始化的全局变量和静态变量  
常量区/数据段 程序结束后由系统释放 初始化的全局变量,静态变量,常量数据  
代码段 程序结束后由系统释放 代码的二进制文件(程序的代码/指令)  

img

栈区的特点

栈是程序中最重要的概念之一, 没有栈就没有函数, 没有局部变量, 栈遵循FILO(First In Last Out)规则。

栈保存了一个函数调用所需要的维护信息, 称为堆栈帧(Stack Frame).。堆栈帧一般包括如下几个方面的内容 :

1. 函数的返回地址和参数
2. 临时变量:包括函数的非静态局部变量以及编译器自动生成的其他临时变量
3. 保存的上下文:包括在函数调用前后需要保持不变的寄存器

一个堆栈帧用两个寄存器划定范围 : ebpesp

  • esp寄存器 : 始终指向栈的顶部
  • ebp寄存器 : 指向堆栈帧的一个固定位置, 又称帧指针(Frame Pointer).

171862021-7e1ba8b07c352099

函数的返回地址 : ebp-4

压入栈中的参数地址 : 分别是 ebp-8, ebp-12等示参数的数量和大小而定.

ebp所直接指向的数据是调用该函数前ebp的值, 这样在函数返回的时候, ebp可以通过读取这个值恢复到调用前的值.

解决栈区空间不足的问题。如果只有栈,那么函数返回的时候栈上的数据就会全部被pop掉,无法将数据传给函数外部,这样的话全局变量则无法动态地产生与销毁。堆区大小由系统决定,包括:系统内存,磁盘交换空间。

系统使用链表来管理堆区的内存分配情况:

1.首先应该知道操作系统有一个记录空闲内存地址的链表。
2.当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。
3 .由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中 

程序员只需要负责堆区的内存分配和释放工作

特点:

  1. 所有程序共享

  2. 存储大数据

  3. 堆内存需要手动管理

  4. 堆区地址不连续

  5. 速度没有栈区快。因为我们要访问堆区中创建对象的属性, 必须先需要通过变量找到栈区的地址,再通过地址定位到到堆区中

    某一个位置, 只有找个这个位置之后,我们才可以访问到存储到这个对象中属性对应的数值.由于有了 这个地址寻找的过程,所

    有速度没有栈区的快。

堆VS栈

具体地说,现代计算机(串行执行机制),都直接在代码底层支持栈的数据结构。这体现在,有专门的寄存器指向栈所在的地址,有专门的机器指令完成数据入栈出栈的操作。这种机制的特点是效率高,支持的数据有限,一般是整数,指针,浮点数等系统直接支持的数据类型,并不直接支持其他的数据结构。因为栈的这种特点,对栈的使用在程序中是非常频繁的。对子程序的调用就是直接利用栈完成的。机器的call指令里隐含了把返回地址推入栈,然后跳转至子程序地址的操作,而子程序中的ret指令则隐含从堆栈中弹出返回地址并跳转之的操作。C/C++中的自动变量是直接利用栈的例子,这也就是为什么当函数返回时,该函数的自动变量自动失效的原因。

和栈不同,堆的数据结构并不是由系统(无论是机器系统还是操作系统)支持的,而是由函数库提供的。基本的malloc/realloc/free 函数维护了一套内部的堆数据结构。当程序使用这些函数去获得新的内存空间时,这套函数首先试图从内部堆中寻找可用的内存空间,如果没有可以使用的内存空间,则试图利用系统调用来动态增加程序数据段的内存大小,新分配得到的空间首先被组织进内部堆中去,然后再以适当的形式返回给调用者。当程序释放分配的内存空间时,这片内存空间被返回内部堆结构中,可能会被适当的处理(比如和其他空闲空间合并成更大的空闲空间),以更适合下一次内存分配申请。这套复杂的分配机制实际上相当于一个内存分配的缓冲池(Cache),使用这套机制有如下若干原因:

  1. 系统调用可能不支持任意大小的内存分配。有些系统的系统调用只支持固定大小及其倍数的内存请求(按页分配);这样的话对于大量的小内存分类来说会造成浪费。
  2. 系统调用申请内存可能是代价昂贵的。系统调用可能涉及用户态和核心态的转换。
  3. 没有管理的内存分配在大量复杂内存的分配释放操作下很容易造成内存碎片。

堆和栈的对比

从以上知识可知,栈是系统提供的功能,特点是快速高效,缺点是有限制,数据不灵活;而栈是函数库提供的功能,特点是灵活方便,数据适应面广泛,但是效率有一定降低。栈是系统数据结构,对于进程/线程是唯一的;堆是函数库内部数据结构,不一定唯一。不同堆分配的内存无法互相操作。栈空间分静态分配和动态分配两种。静态分配是编译器完成的,比如自动变量(auto)的分配。动态分配由alloca函数完成。栈的动态分配无需释放(是自动的),也就没有释放函数。为可移植的程序起见,栈的动态分配操作是不被鼓励的!堆空间的分配总是动态的,虽然程序结束时所有的数据空间都会被释放回系统,但是精确的申请内存/ 释放内存匹配是良好程序的基本要素。

1.碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出,详细的可以>参考数据结构,这里我们就不再一一讨论了。

2.生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。

3.分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。

4.分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。

主要的区别由以下几点:

管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak.

空间大小:一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M(好像是,记不清楚了)。

堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。所以,我们推荐大家尽量用栈,而不是用堆。

另外对存取效率的比较:代码:

char s1[] = “aaaaaaaaaaaaaaa”;

char *s2 = “bbbbbbbbbbbbbbbbb”;

aaaaaaaaaaa是在运行时刻赋值的;

而bbbbbbbbbbb是在编译时就确定的;

但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。

比如:

void main() {  	
	char a = 1;  
	char c[] = "1234567890";  
	char *p ="1234567890";  
	a = c[1];  a = p[1];  
	return;  
} 

对应的汇编代码

10: a = c[1];

00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh]

0040106A 88 4D FC mov byte ptr [ebp-4],cl

11: a = p[1];

0040106D 8B 55 EC mov edx,dword ptr [ebp-14h]

00401070 8A 42 01 mov al,byte ptr [edx+1]

00401073 88 45 FC mov byte ptr [ebp-4],al

第一种在读取时直接就把字符串中的元素读到寄存器cl中,而第二种则要先把指针值读到edx中,在根据edx读取字符,显然慢了。

无论是堆还是栈,都要防止越界现象的发生(除非你是故意使其越界),因为越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈结构,产生以想不到的结果,就算是在你的程序运行过程中,没有发生上面的问题,你还是要小心,说不定什么时候就崩掉,编写稳定安全的代码才是最重要的。

全局变量、静态变量、常量

教科书中 全局变量 和 静态变量 的存储区域

  1. 有初始值的全局变量 和 静态变量 保存在 数据段(常量区)
  2. 没有初始值的全局变量 和 静态变量 保存在 BSS 段(静态区) 当给 全局变量 或 静态变量 设置初始值后,会被移动到 数据段(常量区)

Xcode 8 中 全局变量 和 静态变量 的存储区域

  1. 无论是否设置初始值,全局变量 和 静态变量都保存在 BSS 段(静态区)
  2. 常量 存储在 数据段(常量区)

静态变量

static关键字的作用:

  1. 在 BSS 段为 静态变量 分配空间
  2. 为 静态变量 设置初始值,如果没有指定初始值,会使用 0 来作为初始值
  3. static 关键字定义静态变量的代码,只会被执行一次

常量

  1. const 关键字保证其后修饰的常量的值不允许被修改

    常量的工作原理:

    在程序被加载到内存时,就会为常量分配空间并且设置初始值。在数据段为常量分配空间,如果没有指定初始值,会使用 0 作为初始值。

  2. 不能把 常量 定义在头文件中,应该在 .m 中定义常量

    (跟全局变量不能定义在头文件中一样,会导致重复定义)

  3. 在 .h 中使用 extern 关键字声明该 常量 在其他文件设置值

static用来控制变量的存储方式和可见性

函数内部定义的变量,在程序执行到它的定义处时,编译器为它在栈上分配空间,函数在栈上分配的空间在此函数执行结束时会释放掉,这样就产生了一个问题: 如果想将函数中此变量的值保存至下一次调用时,如何实现? 最容易想到的方法是定义一个全局的变量,但定义为一个全局变量有许多缺点,最明显的缺点是破坏了此变量的访问范围(使得在此函数中定义的变量,不仅仅受此函数控制)。

需要一个数据对象为整个类而非某个对象服务,同时又力求不破坏类的封装性,即要求此成员隐藏在类的内部,对外不可见。

static的内部机制:

静态数据成员要在程序一开始运行时就必须存在。因为函数在程序运行中被调用,所以静态数据成员不能在任何函数内分配空间和初始化。

这样,它的空间分配有三个可能的地方,一是作为类的外部接口的头文件,那里有类声明;二是类定义的内部实现,那里有类的成员函数定义;三是应用程序的main()函数前的全局数据声明和定义处。

静态数据成员要实际地分配空间,故不能在类的声明中定义(只能声明数据成员)。类声明只声明一个类的“尺寸和规格”,并不进行实际的内存分配,所以在类声明中写成定义是错误的。它也不能在头文件中类声明的外部定义,因为那会造成在多个使用该类的源文件中,对其重复定义。

static被引入以告知编译器,将变量存储在程序的静态存储区而非栈上空间,静态数据成员按定义出现的先后顺序依次初始化,注意静态成员嵌套时,要保证所嵌套的成员已经初始化了。消除时的顺序是初始化的反顺序。

static的优势:

可以节省内存,因为它是所有对象所公有的,因此,对多个对象来说,静态数据成员只存储一处,供所有对象共用。静态数据成员的值对每个对象都是一样,但它的值是可以更新的。只要对静态数据成员的值更新一次,保证所有对象存取更新后的相同的值,这样可以提高时间效率。