1. 内核空间和用户空间
在32位的系统下,内存拥有4G(2^32)的寻址能力。大多数操作系统会将4G空间中的一部分给内核使用,应用程序无法直接访问,这块内存称为内核内存空间。
- Windows系统会将高地址的2G空间分给内核(可配置为1G)
- Linux默认是将高地址1G空间分给内核
剩余空间按称为用户内存空间。
2. 内存区域
- 栈:通常在用户内存空间的最高地址处分配,一般数M大小。
- 堆:通常在栈的低地址方向,也可能内存位置不固定,一般比栈大很多。
- 可执行文件映像:由装载器在装载时将可执行文件内存读取到此部分位置。
- 保留区:内存中收到保护而禁止访问的内存区域的总称,例如大多数操作系统中极小的地址是不允许访问的,如指针指向nullptr(NULL)。所以在清理完之后将指针指向nullptr。这里多说一下,在清理指针或者初始化指针的时候尽量使用nullptr/nullptr_t,而不是NULL。
- 动态链接库映射区:用于装载动态链接库,linux中如果可执行文件依赖其它共享库,那么系统会为此文件从0x40000000的地址分配相应空间,载入动态库。
- 代码段:存储程序执行代码的空间,其大小在程序运行前已经确定,并且通常是只读的状态。某些架构中也允许代码段可写,可修改程序。也有可能存放一些制度的常量。
- 数据段:存储程序中已经初始化的全局变量的空间。
- 未初始化数据段(BSS段):存储程序中未初始化或者初始化未0的全局变量和静态变量的空间。
栈从高到低增长,堆从低到高增长
3. 实现方式
操作系统是通过内存管理器实现内存的申请和划分的,并且负责将虚拟内存地址和物理内存地址映射起来。映射方式是内部的结构体存储指向空间地址。
4. malloc的实现
将内存的处理交给内核去做无疑是可行的,但是系统中存在着无数程序的无数申请和释放内存操作,内核调用的性能消耗是很大的,进而影响应用程序性能。
比较好的做法是程序向操作系统申请空间,由程序自己的运行库去管理,当此处空间被使用完,再根据需要向操作系统申请。
5. Linux的堆管理
Linux下堆分配方式有两种系统调用,bk()和mmap(),分配的都是虚拟内存空间。标准C语言库中的malloc/free进行申请和释放内存,底层都是由brk/mmap/munmap实现的。
- brk():设置进程数据段的结束地址,可以扩大或者缩小数据段(linux系统中的数据段和BSS段统称数据段)。如果将数据段的结束地址向高地址位移动,那么扩大的部分就可以供程序使用,然后将这部分作为堆使用。
- mmap():向操作系统申请一块虚拟内存空间,此空间位于文件映射区,就是堆和栈中间。linux中的glibc的malloc是小于128k的空间在现有堆中分配出来,大于128k的空间调用mmap函数分配一块匿名空间,在匿名空间中分配空间。
6. 最大malloc(linux)
最大申请空间受到诸多因素干扰,比如系统限制或者物理内存和交换空间总和等。mmap申请匿名空间时系统会为它在内存或交换空间中预留下地址,但是不能超过空闲内存和空闲交换空间的总和。
7. 堆分配算法
- malloc
将堆中各个空闲块根据链表链接。申请空间时遍历查找合适大小,拆分出来;释放空间时则合并进链表中。最后链表存在很多细碎空间,在申请一块大内存时候malloc会请求延时,以便整理细碎空间,合并成一块大内存。- 位图
将堆划分为大量相等的块。申请空间时,分配整倍数的块大小,第一个块称为head,其余称为body,我们可以使用一个整数数组记录块的使用情况。每个块均只有头、主体和空闲三种状态,那么只需要两位就可以表示出来所有块的状态,所以称为位图。- 对象池
将堆空间划分为大小相等的块(与位图类似),它认为某种情况下每次分配的空间都相等,所以每次返回一个块的大小,可以变化为链表或者位图。因为不用每次查找合适的大小内存,所以效率很高。
实际上,堆的分配算法是多种算法复合而成的,并不是单一的算法。