詳解Go是如何優(yōu)雅的進行內存管理
前言
Go語言拋棄C/C++中的開發(fā)者管理內存的方式,實現(xiàn)了主動申請與主動釋放管理,增加了逃逸分析和垃圾回收,將開發(fā)者從內存管理中釋放出來。
所以我們在日常編寫代碼的時候不需要精通內存的管理,它確實很復雜。但是另一方面,如果你掌握了Go內存管理的基本概念和知識點,可以讓你寫出更高質量的,更壓榨機器性能的代碼;另外,還能幫助你更快更精準得定位Bug,快速解決問題。所以,作為進階的Go開發(fā),了解掌握Go的內存管理還是很有必要的。
相關背景
存儲金字塔
馮·諾依曼計算機體系中的存儲器,是用于存儲程序和數(shù)據的?,F(xiàn)代計算機系統(tǒng)中,一般都是采用“CPU寄存器-CPU高速緩存-內存-硬盤”的存儲器結構。自上而下容量逐漸增大,速度逐漸減慢,單位價格逐漸降低。
1、CPU寄存器:存儲CPU正在使用的數(shù)據或指令。
2、CPU高速緩存:存儲CPU近期要用到的數(shù)據和指令。
3、內存:存儲正在運行或者將要運行的程序和數(shù)據。
4、硬盤:存儲暫時不使用或者不能直接使用的程序和數(shù)據
虛擬內存
物理內存:是指實際通過物理內存而獲得的內存空間。
虛擬內存:與物理內存相反,是指根據系統(tǒng)需要從硬盤中虛擬的劃出一部分存儲空間。
虛擬內存技術就是對內存的一種抽象,有了這層抽象之后,程序運行進程的總大小可以超過實際可用的物理內存大小。每個進程都有自己的獨立虛擬地址空間,然后通過CPU和MMU把虛擬內存地址轉換為實際物理地址。
TCMalloc
TCMalloc全稱是Thread Cache Malloc,是google為C語言開發(fā)的內存分配算法,是Go內存分配的起源。
TCMalloc內存分配算法的核心思想是把內存分為多級管理,從而降低鎖的粒度,它將可用的堆內存采用二級分配的方式進行管理,每個線程都會自行維護一個獨立的線程內存池,進行內存分配時優(yōu)先從該線程內存池中分配, 當線程內存池不足時才會向全局內存池申請,以避免不同線程對全局內存池的頻繁競爭 ,進一步的降低了內存并發(fā)訪問的粒度。
Go的內存分配算法是基于TCMalloc內存分配算法實現(xiàn)的,借鑒了TCmalloc的思想。
幾個重要概念
Page: 操作系統(tǒng)對內存的管理同樣是以頁為單位,但TCMalloc中的Page和操作系統(tǒng)的中頁是倍數(shù)關系,x64下Page大小為8KB。
Span:一組連續(xù)的Page被叫做Span,是TCMalloc內存管理的基本單位,有不同大小的Span,比如2個Page大的Span,16個Page大的Span。
ThreadCache:每個線程各自的Cache,每個ThreadCache包含多個不同規(guī)格的Span鏈表,叫做SpanList,內存分配的時候,可以根據要分配的內存大小,快速選擇不同大小的SpanList,在SpanList上選擇合適的Span,每個線程都有自己的ThreadCache,所以ThreadCache是無鎖訪問的。
CentralCache:中心Cache,所有線程共享的Cache,也是保存的SpanList,數(shù)量和ThreadCache中數(shù)量相同,當ThreadCache中內存不足時,可以從CentralCache中獲取,當ThreadCache中內存太多時,可以放回CentralCache,由于CentralCache是線程共享的,所以它的訪問需要加鎖。
PageHeap:堆內存的抽象,同樣當CentealCache中內存太多或太少時,都可從PageHeap中放回或獲取,同樣,PageHeap的訪問也是需要加鎖的。
管理分配
核心思想
Go在程序啟動的時候,會分配一塊連續(xù)的內存(虛擬的地址空間,還沒有真正地分配內存),切成小塊后自己進行管理,對內存的分配遵循以下思想。
1.每次從操作系統(tǒng)申請一大塊內存, 以減少系統(tǒng)調用。
2.將申請到的大塊內存按照特定大小預先切分成小塊, 構成鏈表。
3.為對象分配內存時, 只需從大小合適的鏈表提取一個小塊即可。
4.回收對象內存時, 將該小塊內存重新歸還到原鏈表, 以便復用。
5.如閑置內存過多, 則嘗試歸還部分內存給操作系統(tǒng), 降低整體開銷。
內存管理由mcache、mcentral、mheap組成一個三級管理結構,本質上都是對mspan的管理,三者用于不同的目的來共同配合管理所有mspan。
mspan
mspan是Go中內存管理的基本單元,是由一片連續(xù)的8kB的page組成的內存塊。但是小對象和大對象分配的位置不用,大對象在mheap上分配,mheap向操作系統(tǒng)申請新內存時,是向虛擬內存申請;小對象使用mcache的tiny分配器分配。
一組連續(xù)的Page組成1個Span,go把內存分為67個大小不同的span,并且大小是不固定的。
源碼文件src/runtime/sizeclasses.go對67種span的定義(源碼版本為go-1.17.1,本文下所有源碼展示均為此版本)
延伸擴展:67種定義列表里面有一列的名稱叫做"max waste",代表的是這個span下可能出現(xiàn)的最大內存浪費比例。舉個例子解釋,看第4個規(guī)格的情況:
class bytes/obj bytes/span objects tail waste max waste min align 4 32 8192 256 0 21.88% 32
4的對象最小內存長度為25字節(jié)(因為小于25的只會申請3或3以下的,要不到4),所以如果每個object都被25字節(jié)的對象申請,此時內存浪費最大,對應浪費率為:(32-25)/32 = 21.88%。
再通過觀察整個列表可以看到,"max waste"一列并非線性遞減的,熟悉Linux的同學應該猜到原因了,沒錯,這個設計跟大名鼎鼎的伙伴算法是非常相似的。
伙伴算法(buddy算法),就是將內存分成若干塊,然后以最適合的方式滿足程序內存需求的一種內存管理算法,伙伴算法是盡可能地在提高內存利用率的同時減少內存碎片。但是算法中,一個很小的塊往往會阻礙一個大塊的合并,一個系統(tǒng)中,對內存塊的分配,大小是隨機的,一片內存中僅一個小的內存塊沒有釋放,旁邊兩個大的就不能合并,這也是造成上面現(xiàn)象的根因。(完整解讀伙伴算法需要非常大的篇幅和難度,本文就不展開了,文章最后有參考鏈接,讀者可自行研究)
回來主題,上面說到的Spans有3種類型:
空閑-span,沒有對象,可以釋放回操作系統(tǒng),或重用于堆分配,或重用于堆棧內存。
正在使用-span,至少有一個堆對象,可能有更多的空間。
棧-span,用于 goroutine 堆棧。此跨度可以存在于堆棧中或堆中,但不能同時存在。
源碼文件src/runtime/mheap.go對mspan結構體的定義
type mspan struct { next *mspan // 鏈表后向指針 prev *mspan // 鏈表前向指針 list *mSpanList // 雙端隊列的head(已無實際用途) startAddr uintptr // span起始位置的地址指針 npages uintptr // 可供分配的頁數(shù) ... manualFreeList gclinkptr // 在mSpanManual的空閑對象 allocCache uint64 // 在freeindex處的allocBits的緩存 ... allocBits *gcBits // 標記span中的elem哪些是被使用的,哪些是未被使用的 gcmarkBits *gcBits // 標記span中的elem哪些是被標記的,哪些是未被標記的 speciallock mutex // 互斥鎖 }
管理組件說明
內存管理器由mcache, mcentral, mheap3種組件構成: 三級管理結構是為了方便對span進行管理,加速對span對象的訪問和分配,這三個結構在runtime中分別有對應的mcache.go、mcentral.go、mheap.go文件。
mcache:保存的是各種大小的Span,并按Span class分類,小對象直接從mcache分配內存,它起到了緩存的作用,并且可以無鎖訪問Go中是每個P擁有1個mcache,因為在Go程序中,當前最多有GOMAXPROCS個線程在運行,所以最多需要GOMAXPROCS個mcache就可以保證各線程對mcache的無鎖訪問。
mcentral:是所有線程共享的緩存,需要加鎖訪問,它按Span class對Span分類,串聯(lián)成鏈表,當mcache的某個級別Span的內存被分配光時,它會向mcentral申請1個當前級別的Span。
mheap:是堆內存的抽象,把從OS(系統(tǒng))申請出的內存頁組織成Span,并保存起來。當mcentral的Span不夠用時會向mheap申請,mheap的Span不夠用時會向OS申請,向OS的內存申請是按頁來的,然后把申請來的內存頁生成Span組織起來,同樣也是需要加鎖訪問的。mheap主要用于大對象的內存分配,以及管理未切割的mspan,用于給mcentral切割成小對象。
熟悉的金字塔,熟悉的結構
通俗的理解:mcache, mcentral, mheap就是對ThreadCache, CentralCache, PageHeap的繼承沿用和基于go體系的優(yōu)化處理版本。
分配流程
Go的內存分配器在分配對象時,根據對象的大小,分成三類:小對象(<=16B)、一般對象(>16B && <=32KB)、大對象(>32KB)。
源碼文件src/runtime/malloc.go根據分配對象的大小選擇對應的空間申請
大體上的分配流程:
1.>32KB 的對象,直接從mheap上分配。
2.<=16B 的對象使用mcache的tiny分配器分配。
3.>16B && <=32KB 的對象,首先計算對象的規(guī)格大小,然后使用mcache中相應規(guī)格大小的mspan分配。
如果mcache沒有相應規(guī)格大小的mspan,則向mcentral申請; 如果mcentral沒有相應規(guī)格大小的mspan,則向mheap申請; 如果mheap中也沒有合適大小的mspan,則向OS申請。
源碼文件src/runtime/mheap.go內存分配初始化過程
小結
Go內存管理源自TCMalloc,優(yōu)秀作品源于繼承和優(yōu)化(在這里我自己想到了一句話:如果說我比別人看得更遠些,那是因為我站在了巨人的肩上--牛頓)。但它比TCMalloc還多了2件東西:逃逸分析(后面篇幅會提及)和垃圾回收。
總結一下它在底層設計上著重用到的2個重要觀念:
使用緩存提高效率:在存儲的整個體系中到處可見緩存的思想,利用緩存減少了系統(tǒng)調用的次數(shù),降低了鎖的粒度、減少加鎖的次數(shù),提高了管理效率。
以空間換時間:空間換時間是一種常用的性能優(yōu)化思想,數(shù)據庫的索引/許多數(shù)據結構的本質就是空間換時間。
關聯(lián)知識點
逃逸分析
Go堆內存所使用的內存頁與goroutine的棧所使用的內存頁是交織在一起的,帶GC(垃圾回收)功能的GO語言會對位于堆上的對象進行自動管理。當某個對象不可達時,即沒有其對象引用它時,它將會被回收并被重用(三色標記)。但GC是會給程序帶來性能損耗的,尤其是當堆內存上有大量待掃描的堆內存對象時,將會給GC帶來過大的壓力,從而消耗更多的計算和存儲資源。于是開發(fā)者們都想盡量減少在堆上的內存分配,可以在棧上分配的變量盡量留在棧上。
逃逸分析(escape analysis)就是在程序編譯階段根據程序代碼中的數(shù)據流,對代碼中哪些變量需要在棧上分配,哪些變量需要在堆上分配進行靜態(tài)分析的方法。
分析準則: 逃逸分析是在編譯器完成的,也就是只存在于編譯階段;如果變量在函數(shù)外部沒有引用,則優(yōu)先放到棧中;如果變量在函數(shù)外部存在引用,則必定放在堆中。
命令:go build -gcflags '-m -m -l' xxx.go
內存對齊
CPU訪問內存時,并不是逐個字節(jié)訪問,而是以字長為單位訪問。這樣是為了是減少CPU訪問內存的次數(shù),提升CPU訪問內存的吞吐量。如果訪問對象在內存的存儲空間是對齊的話,CPU讀取一次即可,否則就要讀取兩次甚至多次,如下圖清晰可見。
對齊規(guī)則
1.第一個成員在與結構體變量偏移量為0的地址處;2.其他成員變量要對齊到對齊數(shù)的整數(shù)倍的地址處;3.結構體總大小為最大對齊數(shù)的整數(shù)倍。
下圖是不同類型的對齊系數(shù)和占用字節(jié)數(shù)
所以我們在日常編碼過程中,要盡量對結構體的變量類型做針對性的順序調整,以符合對齊原則。
One More Thing
介紹完了Go的情況,最后來簡單看下其他語言的,作者本人對Java不太熟悉,就不摻和了,就用最簡單的描述來講一下相對熟悉的php和python。
php:php也是有一個基本的分配單元叫chunk,chunk分配了512個page,page的大小為4KB。內存分配模式也是有3種,small(小于等于3KB),large(大于3KB小于等于2MB-4KB內存),huge(大于2MB-4KB內存),GC機制是引用計數(shù)方式,對堆區(qū)zend_mm_heap的管控就相對非常隨意了(純個人心得理解)。
python:py最大的特色是有個內存池,可以減少內存碎片化,提高執(zhí)行效率?;厥諜C制也是引用計數(shù),但是它有標記/清除和分代回收兩個輔助功能。
綜合3種語言對比,可以看到既有共同交集的地方,也有各自的私有屬性特色,各自的管理分配方式用到自己的語言環(huán)境下都能發(fā)揮最大的作用和效率。這也驗證了一句至高的哲學:方案設計或者架構理念,是沒有最優(yōu)秀最完美的,但是會有最適合最貼近使用場景的。
以上就是詳解Go是如何優(yōu)雅進行內存管理的詳細內容,更多關于Go內存管理的資料請關注腳本之家其它相關文章!
相關文章
go json數(shù)據轉發(fā)的實現(xiàn)代碼
這篇文章主要介紹了go json數(shù)據轉發(fā)的實現(xiàn)代碼,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-09-09