前面說過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分配的關鍵就是在於採用了一些策略,好比多個變長鏈表的散列策略,好比高速緩存策略以及通常緩存策略,考慮到的緣由就是通常小內存的使用率比大內存要大,所以有必要爲小內存維護一個高速的池,另外小內存的釋放頻率也高,通常都是用於存放一些臨時數據的,所以爲小內存維護一個池不會對其它需求不公平,緩存的優點在於它容量通常比較小,遍歷查找很快,而且裏面的數據幾乎都是熱的,,在這裏,所謂的數據要過熱的意義不是指內存塊的內存,而是內存塊的大小,一個內存塊比較熱的意思是說該大小的內存塊被頻繁使用,另外一句話說就是隻有頻繁使用的數據纔會進入緩存,而且這些數據的量不會太多,若是太多的話緩存就失去了它的查找優點,若是數據不熱的話,查找就會頻繁失敗,最終仍是要進入通常的分配模式,所以設計一個緩存系統要考慮的問題不少,毫不僅僅是理論上那麼簡單。還有一個策略就是堆頂的特殊處理,堆頂不放在任何一個鏈表中,對它進行照顧就是由於爲了更有效的將內存退還操做系統,由於堆的壓縮只能從堆頂開始,操做系統只知道給了一個進程虛擬內存連續的一大塊叫作堆的內存,別的什麼也不知道,應用程序歸還的時候一樣須要連續的從堆頂歸還而不能僅僅歸還空洞,歸根結底要對堆頂進行特殊的處理