光有栈对于面向对象过程的程序设计远远不够,因为栈上的数据在函数返回的时候就会被释放带哦,所以无法将数据传递至函数外部。而全局变量没有办法动态地产生,只能在编译的时候定义,有很多情况下缺乏表现力。在这种情况下,堆(Heap)是唯一的选择
堆是一块巨大的内存空间,常常占据整个虚拟内存空间的绝大部分。在这片空间里,程序可以请求一块连续内存,并自由地使用,这块内存在程序主动放弃之前都会一直保持有效。下面是一个申请堆空间最简单的例子。
1 int main() 2 { 3 char * p= (char*)malloc(1000); 4 /*use p as an array of size of 1000*/ 5 free(p); 6 }
在第三行用malloc申请了1000个字节的空间之后,程序可以自由地使用这1000个字节,直到程序用free函数释放它。
堆分配算法
对于程序来说,堆空间只是程序向操作系统申请划出来的一大块地址空间。而程序在通过malloc申请内存空间的大小却是不一定的。从数个字节到数个GB都是有可能的。于是我们必须将堆空间管理起来,将它分块地按照用户需求出售给最终的程序,并且还可以按照一定的方式收回内存。其实这个问题可以归结为:如何管理一大块内存空间,能够按照需求分配、释放其中的空间,这就是堆分配算法。
1. 空闲链表
空闲链表(Free List)的方法实际上就是把堆中各个空闲的块按照链表的方式连接起来,当用户请求一块空间时,可以遍历整个列表,直到找到合适大小的块并且将它拆分:当用户请求一块内存空间时,可以遍历整个列表,直到找到合适大小的块并且将它拆分:当用户释放空间时将它合并到空闲链表中。
我们首先需要一个数据结构来等级堆空间里所有需哟啊的空闲时间,这样才能知道程序请求空间的时候该分配给它哪一块内存。这样的结构有很多种,这里介绍最简单的一种——空闲链表。
空闲链表是这样一种数据结构,在堆里的每一个空闲空间的开头(或结尾)有一个头(header),头结构里记录了上一个(prev)和下一个(next)空闲块的地址,也就是说,所有的空闲块形成了一个链表。如下图所示:
首先在空闲链表里查找足够容纳请求大小的一个空闲块,然后将这个块分成两部分,一部分为请求的空间,另一部分剩余下来的空闲空间。下面将链表里对应原来空闲块的结构更新为新的剩下的空闲块,如果剩下的空闲块大小为0,则直接将这个结构从链表里删除。下图掩饰了用户请求一块喝空闲块恰好相等的内存后堆的状态
这样的空闲链表实现尽管简单,但在释放空间的时候,给定一个已分配块的指针,堆无法确定这个块的大小。一个简单的解决方法是当用户请求k个字节空间的时候,我们实际分配k+4个字节,这4个字节用于存储该分配的大小,即k+4,。这样释放该内存的时候只要看看这4个字节的值,就能知道该内存的大小,然后将其插入到空闲链表里就可以了。
当然这仅仅是最简单的一种分配策略,这样的思路存在很多问题。例如,一旦链表被破坏,或者记录长度的那4个字节被破坏,整个堆就无法正常工作,而这些恰恰很容易被越界读写所接触到。
2.位图
使用位图方法时,内存可能被划分成小到几个字或大到几千字节的分配单元。每个分配单元对应于位图中的一位,0表示空闲,1表示占用。
分配单元的大小是一个重要的设计因素。分配单元越小,位图越大。然而即使只有4个字节大小的分配单元,32位的内存也只需要位图中的1位;32n的内存需要n位的位图,所以位图只占用了1/33的内存。若选择比较大的分配单元,则位图更小。但若进程的大小不是分配单元的整数倍,那么在最后一个分配单元中就会有一定数量的内存被浪费了。
因为内存的大小和分配单元的大小决定了位图的大小,所以它提供了一种简单的利用一块固定大小的内存区就能对内存使用情况进行记录的方法。这种方法的主要问题是,在决定把一个占k个分配单元的进程调入内存时,存储管理器必须搜索位图,在位图中找出k个连续的0串。查找位图中指定长度的连续0串是耗时的操作(因为在位图中该串可能跨越字的边界),这是位图的缺点。