堆的数据结构:一般包括堆块和堆表两部分
堆块:由块首和块身组成
块首:堆块头部的几个字节,用来标识该堆块的信息,如块的大小,状态为空闲还是占用等。
块身:紧跟在块首后面的部分,这部分将最终分配给用户。
堆表:一般位于堆区起始位置,用于索引堆区中所有堆块的重要信息,如堆块的位置、大小、状态。
在windows中,占用态的堆块被使用它的程序索引,堆表只索引所有空闲态的堆块。
最重要的堆表有两种:空闲双向链表(空表Freelist)和快速单向链表(快表Lookaside)
空闲堆块由块首的一对指针组织成双向链表。
空表:按照堆块的大小不同,总共分为128条,椎区一开始有一个128项的指针数据,叫空表索引(Freelist array),每一项包括两个指针,用于标识一条空表。如下图所示:
free[0]保存了所有大于等于1024字节且小于512KB的堆块,堆块在链表中按升序排列。
对于free[1]到free[127]: 空闲椎块的大小 = 索引值(下标)* 8字节
快表:为加快堆分配采用的一种单向链表,链表中的堆快不会被合并(空闲块块闲被设置为占用态,防止合并)
快表也有128条,组织结构与空表相似,只是按单向链表组织,初始化为空,最多有4个结点。如下图所示:
堆块分配:快表分配,普通空表分配,零号空表分配
快表分配:寻找大小匹配的空闲堆块进行分配,不会出现“找零钱”现象。
空表分配:寻找最小能够满足要求的空闲堆块进行分配,如果没有正好匹配的堆块,则从稍大一点的堆块中割出一块进行分配,剩下的重新链入空表(即为“找零钱”现象)。
堆块释放:将状态改为空闲,链入相应的堆表末尾,分配时也从末尾分配。
堆块合并:当堆管理系统发现两个空闲堆块相邻时,就会进行堆块合并操作,包括,将堆块从链表中卸下、合并、调整新堆块块首信息、重新链入空表。
堆区还有一种操作叫内存紧缩,类似磁盘碎片整理,会对整个堆进行调整,尽量合并可用碎片。
windows把内存按照大小分为三类:
小块:SIZE < 1KB
大块:1KB <= SIZE < 512KB
巨块:SIZE => 512KB
对应分配释放算法也分为三类,如下图所示:
windows堆管理中的几个要点:
- 快表中的空闲块被设置为占用态,故不会被合并。
- 快表只有精确匹配时才会分配,不存在搜索次优解和“找零钱”现象。
- 快表是单链表操作比双链表简单,插入和删除操作都少用很多指令。
- 快表很快,所有总是优先使用快表,失败时才用空表。
- 快表只有4项,因此空表也是被频繁使用的。
调试态堆管理策略与常态堆管理策略有很大差异,集中体现在:
- 调试堆不用快表,只用空表
- 所有堆块被加上了多余16字节尾部来放止溢出,包括8字节的0xAB和8字节的0x00
- 块首标志位不同
占用态堆块数据结构如下图所示(其中堆块的大小Self Size是包括堆首在内的):
占用态堆块数据结构如下图所示: