zoukankan      html  css  js  c++  java
  • 程序员的自我修养十内存

    内存是承载程序运行的介质,也是程序进行各种运算和表达的场所。

    10.1 程序的内存布局

    现代的应用程序都运行在一个内存空间里,在32位系统里,这个内存空间拥有4GB(2的32次方)的寻址能力。现在的应用程序可以直接使用32位地址进行寻址,这被称为平坦的内存模型。在平坦的内存模型中,整个内存是一个统一的地址空间,用户可以使用一个32位的指针访问任意的内存位置。
    大多数操作系统都会将4GB内存空间中的一部分挪给内核使用,应用程序无法直接访问这一段内存,这一部分内存地址被称为内核空间。
    应用程序使用内存空间有如下”默认”的区域:
    • 栈:用于维护函数调用的上下文,离开了栈函数调用就没办法实现
    • 堆:堆是用来容纳应用程序动态分配内存区域,当程序使用malloc或new分配内存时,得到的内存来自堆里。
    • 可执行文件映像:这里存储着可执行文件在内存里的映像,有装载器在装载时将可执行文件的内存读取或映射到这里。
    • 保留区:不是一个单一的内存区域,而是对内存中受到保护而禁止访问的内存区域总称。

    10.2 栈与调用惯例

    10.2.1 什么是栈

    栈被定义为一个特殊的容器,用户可以将数据压入栈中(入栈,push),也可以将已经压入栈中的数据弹出(出栈,pop),但栈这个容器必须遵守一条规格:先入栈的数据后出栈。
    栈是一个具有上面属性的动态内存区域。压栈操作使得栈增大,而弹出操作使栈减小。
    在经典操作系统中,栈总使向下增长的。
    栈保存一个函数调用所需要的维护信息,这常常被称为堆栈帧或活动记录。
    堆栈帧一般包括如下几方面:
    • 函数返回地址和参数
    • 临时变量:包括函数的非静态局部变量以及编译器自动生成的其他临时变量。
    • 保存的上下文:包括在函数调用前后需要保持不变的寄存器。
    在一个i386下的函数总是这样调用的:
    • 把所有或一部分参数压入栈中,如果有其他参数没有入栈,那么使用某些特定的寄存器传递。
    • 把当前指令的下一条指令的地址压入栈中。
    • 跳转到函数体执行
    一个函数的活动记录用ebp和esp这两个寄存器划定范围。esp寄存器始终指向栈的顶部,同时也就是指向当前函数的活动记录的顶部。ebp寄存器指向了函数活动记录的一个固定位置,ebp寄存器又被称为帧指针。

    ebp固定在图中所示的位置,不随这个函数的执行而变化。esp始终指向栈顶,因此随这函数的执行,esp会不断变化。固定不变的ebp可以用来定位函数活动记录中的各个数据。

    10.2.2 调用惯例

    函数的调用方和被调用方对于函数如何调用须要有一个明确的约定,这样的约定就是调用惯例。
    一个调用惯例有如下几方面:
    • 函数参数的传递顺序和方式:最常见的一种是通过栈传递。对于有多个参数的函数,调用惯例要规定函数调用方将参数压栈的顺序:是从左至右,还是从右至左。
    • 栈的维护方式:在函数将参数压栈之后,函数体会被调用,此后需要将压入栈中的参数全部弹出,以使得栈在函数调用前后保持一致。
    • 名字修饰:为了链接的时候堆调用惯例进行区分,调用管理要对函数本身的名字进行修饰。
    多级调用栈布局:
     

    10.2.3 函数返回值传递

    除了参数传递之外,函数与调用方的交互好友一个渠道就是返回值。eax是传递返回值的通道。函数将返回值存储在eax中,返回后函数的调用方在读取eax。对于大于4字节的返回值,采用eax和edx联合返回的方式进行。
    如果返回值太大,C语言在函数返回时会使用一个临时的栈上内存区域作为中转,结果返回值对象会被拷贝两次。

    10.3 堆和内存管理

    堆这片内存面临一个复杂的行为模式:在任意时刻程序可能发出请求,申请一段内存或者释放一段已经申请了的内存,而且申请的大小从几个字节到数GB都可能,我们不能假设程序会一次申请多少空间,所以,堆的管理比较复杂

    10.3.1 什么是堆

    栈上的数据在函数返回的时候就会被释放掉,所以无法将数据传递至函数外部。全局变量没有办法动态产生数据,只能在编译器的时候定义。这这种情况下,堆是唯一选择。
    堆是一块巨大的内存空间,常常占据了整个虚拟空间的绝大部分。

    10.3.2 Linux进程堆管理

    Linux下进程堆管理有两种分配方式,即两个系统调用:
    • brk()系统调用:实际上就是设置进程数据段的结束地址,它可以扩大或者缩小数据段。
      • mmap()系统调用:作用和windows下的VirtualAlloc相似,作用是向操作系统申请一段虚拟地址空间,这块虚拟地址空间可以映射到某个文件,当它不将地址空间映射到某个文件时,我们称做为匿名空间,它可以拿来作为堆空间。

    10.3.3 Windows进程堆管理

    Windows的进程将地址分配给了各种EXE、DLL、堆、栈。
    每个线程默认栈的大小是1MB,在线程启动时,系统就会为它的进程地址空间中分配相应的地址空间作为栈,线程栈的大小可以由创建时CreatThread的参数指定。
    Windows提供一个API叫做VirtualAlloc(),用来向系统申请空间,它要求空间大小必须为页的整数倍。
    堆分配算法在的实现位于堆管理器,堆管理器提供了一套与堆相关的API用来创建、分配、释放和销毁堆空间
    • HeapCreate:创建一个堆
    • HeapAlloc:在一个堆中分配内存
    • HeapFree:释放已经分配的内存
    • HeapDestroy:摧毁一个堆
    每个进程在创建时都会有一个默认堆,这个堆在进程启动时创建,并且直到进程结束都一直存在。默认堆大小为1MB。一个进程中一次性能够分配的最大堆空间取决于最大的那个堆。

    10.3.4 堆分配算法

    如何管理一大块连续的内存空间,能够按照需求分配、释放其中的空间,这就是堆算法。
    空间链表
    空闲链表实际上就是把堆中各个空闲的块按照链表的方式连接起来,当用户请求一块空间时,可以遍历整个列表,直到找到合适大小的块并且将它拆分,当用户释放空间时将它合并到空闲链表中。
    空闲链表时这样一种结构,在堆里的每个空闲空间的大小(或结尾)有一个头(Header),头结构里记录了上一个(prev)和下一个(next)空闲块的地址。所有的空闲块形成一个链表。
    位图
    核心思想:将整个堆划分为大量的块,每个块的大小相同。当用户请求内存的时候,总时分配整数个块给用户,第一个块我们称为已分配区域的头,其余的称为已分配区域的主体。而我们可以使用一个整数数组来记录块的使用情况,由于每个块只有头/主体/空闲三种状态,所以仅仅需要2为即可表示一个块,所以称为位图。
     
    优点:
    • 速度快:由于整个堆的空闲信息存储在一个数组内,所以访问数组时cache容易命中。
    • 稳定性好:为了避免用户越界读写破坏数据,我们只需简单的备份一下位图即可,而且即使部分数据被破坏,也不会导致整个堆无法工作。
    • 块不需要额外信息,易于管理。
    缺点:
    • 分配内存时容易产生碎片。
    • 如果堆很大,或者设定的一个块很小,那么位图将会很大,可能失去cache命中率高的优势,也会浪费一定的空间。
    对象池
    思路:如果每次分配的空间大小都一样,那么就可以按照这个每次请求分配的大小作为一个单位,把整个堆空间划分为大量的小块,每次请求的时候,只需要找到一个小块就可以了。
    对象池的管理方法可以采用空闲链表,也可以采用位图,与它们的区别仅仅在于它假定了每次请求的都是一个固定大小,因此实现起来很容易。由于每次总是只请求一个单位内存,因此请求得到满足的速度非常块,无须查找一个足够大的空间
  • 相关阅读:
    155. 最小栈
    160. 相交链表
    PAT 1057 Stack
    PAT 1026 Table Tennis
    PAT 1017 Queueing at Bank
    PAT 1014 Waiting in Line
    PAT 1029 Median
    PAT 1016 Phone Bills
    PAT 1010 Radix
    PAT 1122 Hamiltonian Cycle
  • 原文地址:https://www.cnblogs.com/Tan-sir/p/7652149.html
Copyright © 2011-2022 走看看