Go內存分配
一. 背景介紹
先了解一下Linux系統內存相關的背景知識,有助於我們瞭解Go的內存分配
1.覆蓋技術
> 在上古時代的內存管理中,如果程序太大,超過了空閒內存容量,就無法把全部程序裝入內存中,這個時候誕生出了一種解決方案,即覆蓋技術,
簡而言之,就是把程序分爲若干個塊,只把哪些需要用到的指令和數據載入內存中,但是這種技術存在一個很嚴重的問題,必須由程序員手動的給程序劃分塊,並確定各個塊之間的調用關係
2.虛擬內存技術
> 覆蓋技術這種方法,非常耗時,而且使得編程的複雜度大大提升,這個時候就又誕生出了一種解決方案,即虛擬內存技術
2.1 虛擬內存技術的原理
即虛擬內存是對內存的一種抽象,有了這層抽象之後,程序運行進程的總大小可以超過實際可用的物理內存大小,每個進程都有自己的獨立虛擬地址空間,然後通過CPU和MMU把虛擬內存地址轉換爲實際物理地址
2.2 虛擬內存技術的分層設計
虛擬內存體系其實是一種分層設計,總共分爲四層
- 虛擬內存
- 內存映射
- 物理內存
- 磁盤
> 進程訪問虛擬內存的流程:進程訪問內存,其實訪問的是虛擬內存,虛擬內存通過內存映射查看當前需要訪問的虛擬內存是否已經加載到了物理內存,
如果已經加載到了物理內存,則取物理內存的數據,
如果沒有加載到物理內存,則從磁盤加載數據到物理內存,並且把物理內存地址和虛擬內存地址更新到內存映射表中
總結
在沒有虛擬內存的遠古時代,物理內存對所有進程都是共享的,多進程同時訪問同一塊物理內存需要加鎖,鎖的粒度是進程級別的,
在引入虛擬內存後,每個進程都有各自的虛擬內存,這個時候是多線程訪問同一個物理內存需要加鎖,鎖的粒度是線程級別的,
可以看到,一步步鎖的粒度的降低。其實在Go的內存分配中也是這種思想:降低內存併發訪問的粒度。
二.TCMalloc
1.簡單介紹
- TCMalloc全稱是Thread Cache Malloc,是google爲C語言開發的內存分配算法,是Go內存分配的起源。
- 在Linux中,存在很多的內存管理庫,其本質都是在多線程下,追求更高的內存管理效率,更快的分配內存,TCMalloc同樣也是如此。
2.核心思想
- 其核心思想:把內存分爲多級管理,從而降低鎖的粒度,它將可用的堆內存採用二級分配的方式進行管理,每個線程都會自行維護一個獨立的線程內存池,進行內存分配時優先從該線程內存池中分配,
當線程內存池不足時纔會向全局內存池申請,以避免不同線程對全局內存池的頻繁競爭 ,進一步的降低了內存併發訪問的粒度。
簡單介紹一下TCMalloc中幾個重要概念
Page
: 操作系統對內存的管理同樣是以頁爲單位,但TCMalloc中的Page和操作系統的中頁是倍數關係,x64下Page大小爲8KBSpan
: 一組連續的Page被叫做Span,是TCMalloc內存管理的基本單位,有不同大小的Span,比如2個Page大的Span,16個Page大的SpanThreadCache
: 每個線程各自的Cache,每個ThreadCache包含多個不同規格的Span鏈表,叫做SpanList,
內存分配的時候,可以根據要分配的內存大小,快速選擇不同大小的SpanList,在SpanList上選擇合適的Span,每個線程都有自己的ThreadCache,所以ThreadCache是無鎖訪問的CentralCache
: 中心Cache,所有線程共享的Cache,也是保存的SpanList,數量和ThreadCache中數量相同
當ThreadCache中內存不足時,可以從CentralCache中獲取
當ThreadCache中內存太多時,可以放回CentralCache
由於CentralCache是線程共享的,所以它的訪問需要加鎖PageHeap
: 堆內存的抽象,同樣當CentealCache中內存太多或太少時,都可從PageHeap中放回或獲取,同樣,PageHeap的訪問也是需要加鎖的
TCMalloc中對不同的對象會區分其大小,不同大小的對象其內存的分配流程也不一樣
- 小對象:0-256KB,分配流程:ThreadCache -> CentralCache -> PageHead, 大部分時候,ThreadCache緩存都是足夠的,不需要去訪問CentralCache和PageHead,不存在鎖,不存在系統調用,分配效率非常高
- 中對象:267KB-1M,分配流程:直接在PageHead中分配
- 大對象:大於1M,分配流程: 直接在large span set(PageHead中一個特殊的地方,專門用於大對象分配)中選擇合適數量的Page組成Span
[comment]: <> ()
[comment]: <> ()
三. Go的內存分配
Go的內存分配和TCMalloc非常類似,僅有少量地方不同
Page
: 和TCMalloc中Page相同Span
: 和TCMalloc中Span相同mcache
: 和TCMalloc中不同之處在於,TCMalloc中是每個線程持,而Go中是每個P(processor,邏輯處理器,go的併發調度器GPM模型中概念)持有,在go程序中,當前最多有GOMAXPROCS個線程在用戶態運行,
所以最多需要GOMAXPROCS個mcache就可以保證各線程對mcache無鎖訪問,而go中線程的運行又是與P綁定的,把mcache交給P剛好mcentral
: 和TCMalloc中CentralCache大致相同,不同之處在於CentralCache是每個size的Span有一個鏈表,mcache是每個size的span有兩個鏈表,這和mcache的內存申請有關,後面再做解釋mheap
: 與TCMalloc中PageHeap大致相同,不同之處在於,mheap把span組織成了樹結構,而不是鏈表,並且還是兩棵樹,利用空間換時間,同樣也是爲了內存的分配效率更快
[comment]: <> ()
go的內存分類不像TCMalloc那樣分成大中小對象,其只分爲小對象和大對象,但其小對象又細分了一個Tiny對象
- 小對象: (mcache -> mcenttral -> mheap 不夠就向右逐級申請)
- Tiny對象:指大小在1byte到16byte之間並且不包含指針的對象
- 其他小對象:大小在16byte到32KB之間的對象
- 大對象:大於32KB的對象,在mheap中分配
再來關注一下go是如何釋放內存的
> go釋放內存的函數是sysUnused,它的功能是給內核提供一個建議:這個內存地址區間的內存已不再使用,可以進行回收,但內核是否進行回收,什麼時候回收,都取決於內核
四.總結
總結一下內存分配中用到的兩個重要思想
- 利用緩存:第一個好處是可以減少系統調用次數,第二個好處是可以降低鎖的粒度,減少加鎖次數(以上兩點,都針對內存分配時)
- 空間換時間,提升速度: 空間換時間是一種常用的性能優化思想,在Hash,Map,二叉樹等數據結構中非常普遍,數據庫索引,Redis等緩存數據庫,都是這種思想