堆简介(heap)_上

堆的特性

(1)堆是一种在程序运行时动态分配的内存。所谓动态是指所需内存的大小在程序设计时 不能预先决定,需要在程序运行时参考用户的反馈。
(2)堆在使用时需要程序员用专用函数进行申请,如 C 语言中的 malloc 等函数、C++中的 new 函数等都是最常见的分配堆内存的函数。堆内存申请有可能成功,也有可能失败,这与申 请内存的大小、机器性能和当前运行环境有关。
(3)一般用一个堆指针来使用申请得到的内存,读、写、释放都通过这个指针来完成。
(4)使用完毕后需要把堆指针传给堆释放函数回收这片内存,否则会造成内存泄露。典型 的释放函数包括 free、delete 等。

堆VS栈对比

在这里插入图片描述

堆的数据结构

堆的数据结构
{
堆块+堆表组成
}

  • 堆块:出于性能的考虑,堆区的内存按不同大小组织成块,以堆块为单位进行标识,而不 是传统的按字节标识。一个堆块包括两个部分:块首块身。块首是一个堆块头部的几个字节, 用来标识这个堆块自身的信息,例如,本块的大小、本块空闲还是占用等信息;块身是紧跟在 块首后面的部分,也是最终分配给用户使用的数据区。 (堆块=块首+块身)

    注意:堆管理系统所返回的指针一般指向块身的起始位置,在程序中是感觉不到块首的存在的。然而,连续地进行内存申请时,如果您够细心,可能会发现返回的内存之 间存在“空隙”,那就是块首!

  • 堆表:堆表一般位于堆区的起始位置,用于索引堆区中所有堆块的重要信息,包括堆块的 位置、堆块的大小、空闲还是占用等。堆表的数据结构决定了整个堆区的组织方式,是快速检索空闲块、保证堆分配效率的关键。堆表在设计时可能会考虑采用平衡二叉树等高级数据结构 用于优化查找效率。现代操作系统的堆表往往不止一种数据结构。

    注意:在 Windows 中,占用态的堆块被使用它的程序索引,而堆表只索引所有空闲态的堆块

最重要的堆表(空表 和 快表)

最重要的堆表有两种:空闲双向链表 Freelist( 简称空表 )和快速单向链表 Lookaside(简称快表)。

空表

空闲堆块的块首中包含一对重要的指针,这对指针用于将空闲堆块组织成双向链表。按照堆块的大小不同,空表总共被分为 128 条。
堆区一开始的堆表区中有一个 128 项的指针数组,被称做空表索引(Freelist array)。该数组的每一项包括两个指针(该数组每项是结构体),用于标识一条空表。


如图下图所示,空表索引的第二项(free[1])标识了堆中所有大小为 8 字节的空闲堆块, 之后每个索引项指示的空闲堆块递增8字节,例如,free[2]标识大小为16字节的空闲堆块,free[3] 标识大小为 24 字节的空闲堆块,free[127]标识大小为 1016 字节的空闲堆块。
因此有: 空闲堆块的大小=索引项(ID)×8(字节)
把空闲堆块按照大小的不同链入不同的空表,可以方便堆管理系统高效检索指定大小的空 闲堆块。需要注意的是,空表索引的第一项(free[0])所标识的空表相对比较特殊。 这条双向链表链入了所有大于等于 1024 字节的堆块(小于 512KB)。这些堆块按照各自的大小在零号空表中升序地依次排列下去。
在这里插入图片描述

快表

快表是 Windows 用来加速堆块分配而采用的一种堆表。这里之所以把它叫做“快表”是因为这类单向链表中从来不会发生堆块合并(其中的空闲块块首被设置为占用态,用来防止堆块合并)。
快表也有 128 条,组织结构与空表类似,只是其中的堆块按照单链表组织。快表总是被初始化为空,而且每条快表最多只有 4 个结点,故很快就会被填满。

堆管理策略

堆中的操作可以分为堆块分配、堆块释放和堆块合并三种。其中,“分配”和 “释放”是在程序提交申请和执行的,而堆块合并则是由堆管理系统自动完成的。

1.堆块分配

堆块分配可以分为三类:快表分配、普通空表分配和零号空表(free[0])分配。
快表中分配堆块比较简单,包括寻找到大小匹配的空闲堆块、将其状态修改为占用态、 把它从堆表中“卸下”、最后返回一个指向堆块块身的指针给程序使用。
普通空表分配时首先寻找最优的空闲块分配,若失败,则寻找次优的空闲块分配,即最小 的能够满足要求的空闲块。
零号空表中按照大小升序链着大小不同的空闲块,故在分配时先从 free[0]反向查找最后一 个块(即表中最大块),看能否满足要求,如果能满足要求,再正向搜索最小能够满足要求的 空闲堆块进行分配(这就明白为什么零号空表要按照升序排列了)。
堆块分配中的“找零钱”现象:当空表中无法找到匹配的“最优”堆块时,一个稍大些的 块会被用于分配。这种次优分配发生时,会先从大块中按请求的大小精确地“割”出一块进行 分配,然后给剩下的部分重新标注块首,链入空表。这里体现的就是堆管理系统的“节约”原 则:买东西的时候用最合适的钞票,如果没有,就要找零钱,决不会玩大方。 由于快表只有在精确匹配时才会分配,故不存在“找钱”现象。
注意::这里没有讨论堆缓存(heap cache)、低碎片堆(LFH)和虚分配。


堆块分配细节
(1)堆块的大小包括了块首在内,即如果请求 32 字节,实际会分配的堆块为 40 字节:8 字节块首+32 字节块身。
2)堆块的单位是 8 字节,不足 8 字节的部分按 8 字节分配。
(3)初始状态下,快表和空表都为空,不存在精确分配。请求将使用“次优块”进行分配。 这个“次优块”就是位于偏移 0x0688 处的尾块。
(4)由于次优分配的发生,分配函数会陆续从尾块中切走一些小块,并修改尾块块首中的 size 信息,最后把 freelist[0]指向新的尾块位置。

2.堆块释放

释放堆块的操作包括将堆块状态改为空闲,链入相应的堆表。所有的释放块都链入堆表的 末尾,分配的时候也先从堆表末尾拿。
另外需要强调,快表最多只有 4 项。

3.堆块合并

经过反复的申请与释放操作,堆区很可能变得“千疮百孔”,产生很多内存碎片。为了合 理有效地利用内存,堆管理系统还要能够进行堆块合并操作,如图所示。
在这里插入图片描述当堆管理系统发现两个空闲堆块彼此相邻的时候,就会进行堆块合并操作。
堆块合并包括将两个块从空闲链表中“卸下”、合并堆块、调整合并后大块的块首信息(如 大小等)、将新块重新链入空闲链表。
注::实际上,堆区还有一种操作叫做内存紧缩(shrink the compact),由RtlCompactHeap 执行,这个操作的效果与磁盘碎片整理差不多,会对整个堆进行调整,尽量合并可用 的碎片。

在具体进行堆块分配和释放时,根据操作内存大小的不同,Windows 采取的策略也会有所 不同。
可以把内存块按照大小分为三类:

  • 小块:SIZE<1KB
  • 大块:1KB≤SIZE<512KB
  • 巨块:SIZE≥512KB

对应的分配和释放算法也有三类,我们可以通过表 5-1-2 来理解 Windows 的堆管理策略。
在这里插入图片描述


强调一下 Windows 堆管理的几个要点。
(1)快表中的空闲块被设置为占用态,故不会发生堆块合并操作。
(2)快表只有精确匹配时才会分配,不存在“搜索次优解”和“找零钱”现象。
(3)快表是单链表,操作比双链表简单,插入删除都少用很多指令。
(4)综上所述,快表很“快”,故在分配和释放时总是优先使用快表,失败时才用空表。
(5)快表只有 4 项,很容易被填满,因此空表也是被频繁使用的。
综上所述,Windows 的堆管理策略兼顾了内存合理使用、分配效率等多方面的因素。

堆数据结构

https://blog.csdn.net/weixin_44309300/article/details/117898717

可参考资料

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值