前面说过glibc实现了malloc,它实现linux系统的堆管理,在linux中没有专有的所谓的API,全部的调用几乎都以c库为根本,所以glibc显得尤其重要,glibc的实现抛开本身的独特策略不说它和windows的实现是同样的,都是维护一个全局的链表,而后每个链表元素由固定大小内存块或者不固定大小的内存块组成,和windows不一样的是,glibc维护了不止一个不定长的内存块链表,而是好几个,每个这种链表负责一个大小范围,这种作法有效减小了分配大内存时的遍历开销,相似于哈希的方式,将很大的范围的数据散列到有限的几个小的范围内而不是全部数据都放在一块儿,虽然最终仍是要在小的范围内查找,可是最起码省去了不少的开销,若是只有一个不定长链表那么就要所有遍历,若是分红3个,就省去了2/3的开销,总之这个策略十分相似于散列。glibc另外的策略就是不止维护一类空闲链表,而是另外再维护一个缓冲链表和一个高速缓冲链表,在分配的时候首先在高速缓存中查找,失败以后再在空闲链表查找,若是找到的内存块比较大,那么将切割以后的剩余内存块插入到缓存链表,若是空闲链表查找失败那么就往缓存链表中查找,这么查找有什么依据吗?其实是有的,正是这个方式让glibc有了本身的策略。linux
这种依据在free的时候体现。若是能合并在堆顶,也就是能和堆顶的空闲元素合并,那么就合并,由于堆的缩减仅仅在堆顶的空闲元素达到必定量的时候才会进行,所以为了尽快将内存归还操做系统,尽可能优先考虑堆顶的释放,可是若是不能合并,好比它和堆顶根本就没有相邻,那么若是该释放的块大小小于80字节,那么就直接将之挂在高速缓存中,为了防止别的块和它合并因此并不更改使用位,这里能够看到,glibc实际上为小于80字节的小内存块维护了一个高速的内存池,若是有小块内存需求,直接今后池中拿走一个便可,只须要从高速缓存摘除之并不须要修改使用位,由于高速缓存中的元素的使用位均为1,这个高速缓存在有大内存块分配需求而且几个分配策略都失败的时候会被回收,回收进空闲链表的过程涉及到相邻块的合并,合并以后就有可能知足稍微大一些的内存分配需求,这里为什么将界限定位为80个字节呢?其实是一个经验值,那么介于80字节和128k字节之间的内存块在释放的时候要将使用位设置为0,而后试图和相邻块合并,而后挂入缓存链表。windows
下面的这段话摘自于一篇文章,比较详细的阐述了glibc的内存分配和释放的过程,前提是分配大小小于128k:缓存
1.堆是经过brk的方式来增加或压缩的,若是在现有的堆中不能找到合适的chunk,会经过增加堆的方式来知足分配,若是堆顶的空闲块超过必定的阀值会收缩堆,因此只要堆顶的空间没释放,堆是一直不会收缩的。ide
2.堆中的分配信息是经过两个方式来记录。第一.是经过chunk的头,chunk中的头一个字是记录前一个chunk的大小,第二个字是记录当前chunk的大小和一些标志位,从第三个字开始是要使用的内存。因此经过内存地址能够找到chunk,经过chunk也能够找到内存地址。还能够找到相邻的下一个chunk,和相邻的前一个chunk。一个堆彻底是由n个chunk组成。第二.是由3种队列记录,只用空闲chunk才会出如今队列中,使用的chunk不会出如今队列中。若是内存块是空闲的它会挂到其中一个队列中,它是经过内存复用的方式,使用空闲chunk的第3个字和第4个字看成它的前链和后链(变长块是第5个字和第6个字),省的再分配空间给它。第一种队列是bins,bins有128个队列,前64个队列是定长的,每隔8个字节大小的块分配在一个队列,后面的64个队列是不定长的,就是在一个范围长度的都分配在一个队列中。全部长度小于512字节(大约)的都分配在定长的队列中。后面的64个队列是变长的队列,每一个队列中的chunk都是从小到大排列的。第二种队列是unsort队列(只有一个队列),(是一个缓冲)全部free下来的若是要进入bins队列中都要通过unsort队列。第三种队列是fastbins,大约有10个定长队列,(是一个高速缓冲)全部free下来的而且长度是小于80的chunk就会进入这种队列中。进入此队列的chunk在free的时候并不修改使用位,目的是为了不被相邻的块合并掉。操作系统
3.malloc的步骤设计
-->先在fastbins中找,若是能找到,从队列中取下后(不须要再置使用位为1了)马上返回。队列
-->判断需求的块是否在小箱子(bins的前64个bin)范围,若是在小箱子的范围,而且恰好有需求的块,则直接返回内存地址;若是范围在大箱子(bins的后64个bin)里,则触发consolidate。(由于在大箱子找通常都要切割,因此要优先合并,避免过多碎片)进程
-->而后在unsort中取出一个chunk,若是能找到恰好和想要的chunk相同大小的chunk,马上返回,若是不是想要chunk大小的chunk,就把他插入到bins对应的队列中去。转3,直到清空,或者一次循环了10000次。内存
-->而后才在bins中找,找到一个最小的能符合需求的chunk从队列中取下,若是剩下的大小还能建一个chunk,就把chunk分红两个部分,把剩下的chunk插入到unsort队列中去,把chunk的内存地址返回。it
-->在topchunk(是堆顶的一个chunk,不会放到任何一个队列里的)找,若是能切出符合要求的,把剩下的一部分看成topchunk,而后返回内存地址。
-->若是fastbins不为空,触发consolidate即把全部的fanbins清空(是把fanbins的使用位置0,把相邻的块合并起来,而后挂到unsort队列中去),而后继续第3步。
-->还找不到话就调用sysalloc,其实就是增加堆了。而后返回内存地址。
4.free的步骤
-->若是和topchunk相邻,直接和topchunk合并,不会放到其余的空闲队列中去。
-->若是释放的大小小于80字节,就把它挂到fastbins中去,使用位仍然为1,固然更不会去合并相邻块。
-->若是释放块大小介于80-128k,把chunk的使用位置成0,而后试图合并相邻块,挂到unsort队列中去,若是合并后的大小大于64k,也会触发consolidate,(多是周围比较多小块了吧),而后才试图去收缩堆。(收缩堆的条件是当前free的块大小加上先后能合并chunk的大小大于64k,而且要堆顶的大小要达到阀值,才有可能收缩堆)
以上的阐述仍是很明确的,glibc分配的关键就是在于采用了一些策略,好比多个变长链表的散列策略,好比高速缓存策略以及通常缓存策略,考虑到的缘由就是通常小内存的使用率比大内存要大,所以有必要为小内存维护一个高速的池,另外小内存的释放频率也高,通常都是用于存放一些临时数据的,所以为小内存维护一个池不会对其它需求不公平,缓存的优点在于它容量通常比较小,遍历查找很快,而且里面的数据几乎都是热的,,在这里,所谓的数据要过热的意义不是指内存块的内存,而是内存块的大小,一个内存块比较热的意思是说该大小的内存块被频繁使用,另外一句话说就是只有频繁使用的数据才会进入缓存,而且这些数据的量不会太多,若是太多的话缓存就失去了它的查找优点,若是数据不热的话,查找就会频繁失败,最终仍是要进入通常的分配模式,所以设计一个缓存系统要考虑的问题不少,毫不仅仅是理论上那么简单。还有一个策略就是堆顶的特殊处理,堆顶不放在任何一个链表中,对它进行照顾就是由于为了更有效的将内存退还操做系统,由于堆的压缩只能从堆顶开始,操做系统只知道给了一个进程虚拟内存连续的一大块叫作堆的内存,别的什么也不知道,应用程序归还的时候一样须要连续的从堆顶归还而不能仅仅归还空洞,归根结底要对堆顶进行特殊的处理