golang內(nèi)存分配
Golang的內(nèi)存分配是由golang runtime完成丧叽,其內(nèi)存分配方案借鑒自tcmalloc眷柔。
主要特點(diǎn)就是
- 為每個(gè)工作線程(P)都維護(hù)了一個(gè)分配cache期虾,小對(duì)象((16,32KB])和微(Tiny)對(duì)象((1B,16B])基本上無需全局鎖驯嘱。
- 為常用的內(nèi)存尺寸(16B镶苞,32KB] 之間內(nèi)存分類為numSpanClass(134-2)種對(duì)象尺寸。位于此大小區(qū)間的內(nèi)存分配都?xì)w到這些spanclass 尺寸的element上進(jìn)行分配鞠评。如此可以減少內(nèi)存碎片的產(chǎn)生茂蚓。
本文中的element指一定大小的內(nèi)存塊是內(nèi)存分配的概念,并為出現(xiàn)在golang runtime源碼中
本文講述x8664架構(gòu)下的內(nèi)存分配
主要結(jié)構(gòu)
Golang 內(nèi)存分配有下面幾個(gè)主要結(jié)構(gòu)
- mspan 用于管理一連串地址連續(xù)的頁面剃幌,golang將同一mspan維護(hù)的地址空間劃分為同一個(gè)尺寸的element聋涨,每個(gè)element用于存儲(chǔ)一個(gè)大小屬于當(dāng)前SpanClass對(duì)象。
- mcache 每個(gè)P都有一個(gè)mcache负乡,在此mcache上有一個(gè)長度為numSpanClass的數(shù)組牍白,里面存儲(chǔ)的每個(gè)成員指向一個(gè)mspan。
- mcentral 全局的抖棘,每個(gè)mcentral用于管理相同的element size的mspan茂腥。
- mheap 全局的狸涌,用于管理golang的整個(gè)堆。在mheap上有numSpanClass個(gè)mcentral數(shù)組础芍。
-
heapArena 在x8664上用于管理64MB的堆空間杈抢。一個(gè)heapArena下有多個(gè)mspan数尿。
內(nèi)存分配方案
微(Tiny)對(duì)象
Tiny對(duì)象是指內(nèi)存尺寸小于16B的對(duì)象仑性,這類對(duì)象的分配使用mcache的tiny區(qū)域進(jìn)行分配。當(dāng)tiny區(qū)域空間耗盡時(shí)刻右蹦,它會(huì)從mcache.alloc[tinySpanClass]指向的mspan中找到空閑的區(qū)域诊杆。當(dāng)然如果mcache中span空間也耗盡,它會(huì)觸發(fā)從mcentral補(bǔ)充mspan到mcache的流程何陆。
小對(duì)象
小對(duì)象是指對(duì)象尺寸在(16B晨汹,32KB]之間的對(duì)象,這類對(duì)象的分配原則是:
1贷盲、首先根據(jù)對(duì)象尺寸將對(duì)象歸為某個(gè)SpanClass上淘这,這個(gè)SpanClass上所有的element都是一個(gè)統(tǒng)一的尺寸。
2巩剖、從mcache.alloc[SpanClass]找到mspan铝穷,看看有無空閑的element,如果有分配成功佳魔。如果沒有繼續(xù)曙聂。
3、從mcentral.allocSpan[SpanClass]的nonempty和emtpy中找到合適的mspan鞠鲜,返回給mcache。如果沒有找到就進(jìn)入mcentral.grow()—>mheap.alloc()分配新的mspan給mcentral。
大對(duì)象的分配
大對(duì)象指尺寸超出32KB的對(duì)象源祈,此時(shí)直接從mheap中分配猛拴,不會(huì)走mcache和mcentral,直接走mheap.alloc()分配一個(gè)SpanClass==0 的mspan表示這部分分配空間霞捡。
總結(jié)
對(duì)于程序分配常用的tiny和小對(duì)象的分配语稠,可以通過無鎖的mcache提升分配性能。mcache不足時(shí)刻會(huì)拿mcentral的鎖弄砍,然后從mcentral中充mspan 給mcache仙畦。大對(duì)象直接從mheap 中分配。
進(jìn)程虛擬地址空間管理
在x8664環(huán)境上音婶,golang管理的有效的程序虛擬地址空間實(shí)質(zhì)上只有48位慨畸。在mheap中有一個(gè)pages pageAlloc成員用于管理golang堆內(nèi)存的地址空間。golang從os中申請(qǐng)地址空間給自己管理衣式,地址空間申請(qǐng)下來以后寸士,golang會(huì)將地址空間根據(jù)實(shí)際使用情況標(biāo)記為free或者alloc檐什。如果地址空間被分配給mspan或大對(duì)象后,那么被標(biāo)記為alloc弱卡,反之就是free乃正。
地址空間的狀態(tài)
Golang認(rèn)為地址空間有以下4種狀態(tài):
- None 地址空間初始狀態(tài)
- Reserved 地址已經(jīng)被golang runtime擁有,但是os并為真正分配婶博,訪問此類地址出發(fā)異常
- Prepared 地址已經(jīng)是Reserved瓮具,但是也并為被OS分配真正地址空間。在Linux系統(tǒng)上凡人,Prepared對(duì)應(yīng)的是地址空間為MADV_FREE 表示os可以在自己認(rèn)為需要的時(shí)候回收這段地址空間名党。
- Ready 地址空間真真正正被os分配。訪問此空間不會(huì)出發(fā)異常挠轴。
Golang同時(shí)定義了下面幾個(gè)地址空間操作函數(shù):
- sysReserved 調(diào)用此函數(shù)后传睹,地址從None轉(zhuǎn)換為Reserved狀態(tài)
- sysAlloc 地址從None轉(zhuǎn)換為Ready狀態(tài),一般都是golang runtime自己內(nèi)存管理對(duì)象的分配使用sysAlloc
- sysFree 地址空間從任意狀態(tài)轉(zhuǎn)換為None
- sysMap 地址空間從Reserved轉(zhuǎn)換為Prepared
- sysUsed 地址空間從Prepared轉(zhuǎn)換為Ready
-
sysUnused 地址空間從Ready轉(zhuǎn)換為Prepared
golang如何管理自己的虛擬地址空間
在mheap結(jié)構(gòu)中岸晦,有一個(gè)名為pages成員欧啤,它用于golang 堆使用虛擬地址空間進(jìn)行管理。其類型為pageAlloc
type pageAlloc struct{
...
summary [summacryLevels][]pallocSum
chunks [1<<pallocChunksL1Bits][1<<pallocChunksL2Bits]
...
}
type pallocSum uint64
type pallocData struct{
pallocBits
scavenged pageBits
}
type pallocBits pageBits
// pallocChunkPages/64 =256/64 =4
type pageBits [pallocChunkPages/64]uint64
pageAlloc 結(jié)構(gòu)表示的golang 堆的所有地址空間启上。其中最重要的成員有兩個(gè):
-
summary 為二維數(shù)組邢隧,組成一個(gè)pallocSum的radix tree。在golang 1.14中x8664 radix tree 為4級(jí)碧绞。最終那一級(jí)節(jié)點(diǎn)表示一個(gè)chunk(2MB)地址空間的分配情況府框。
pallocSum定義為64bit 長整型,它被分為三個(gè)bitmap:start讥邻,max 和end迫靖。start表示這段地址空間從起始地址開始連續(xù)的free的地址空間的頁面數(shù)量;max這段地址空間最大連續(xù)頁的數(shù)量兴使,end 為這段地址空間的最后一個(gè)頁編號(hào)系宜。
chunks 數(shù)組每個(gè)成員表示了地址空間內(nèi)所有chunk里頁面分配和scavenge情況。其結(jié)構(gòu)為pallocData发魄,分為兩個(gè)成員盹牧,pallocBits成員表示已分配頁面的bitmap,scavenged 成員表示已scavenged的頁面的bitmap励幼。
chunk如前所述在x8664上為2MB汰寓,也就是常規(guī)的一個(gè)巨頁頁面大小
當(dāng)alloc mspan時(shí)刻,需要使用pageAlloc苹粟,涉及到下面函數(shù)的使用:(s* pageAlloc)alloc找到一個(gè)地址空間可以容納待分配的頁面數(shù)量有滑,并把這段地址空間標(biāo)記為alloc。
(s*pageAlloc)grow 增長mheap管理的堆地址空間
(s*pageAlloc)free 釋放地址空間(將這段地址空間標(biāo)記為free)
alloc mspan時(shí)嵌削,先使用pageAalloc.alloc嘗試分配一段地址空間毛好,如果沒有分配成功望艺,就使用grow增加一段地址空間映射,最后再使用alloc分配肌访。
空間清掃sweep
在golang的gc流程中會(huì)將未使用的對(duì)象標(biāo)記為未使用找默,但是這些對(duì)象所使用的地址空間并未交還給os。地址空間的申請(qǐng)和釋放都是以golang的page為單位(實(shí)際以chunk為單位)進(jìn)行的吼驶。sweep的最終結(jié)果只是將某個(gè)地址空間標(biāo)記可被分配惩激,并未真正釋放地址空間給os,真正釋放是后文的scavenge過程旨剥。
mspan的sweep
在gc mark結(jié)束以后會(huì)使用sweep()去嘗試free一個(gè)span咧欣;在mheap.alloc 申請(qǐng)mspan時(shí)刻浅缸,也使用sweep去清掃一下轨帜。
清掃mspan主要涉及到下面函數(shù)
- mspan.sweep()掃描這個(gè)span中element的使用情況,當(dāng)最終如果整個(gè)mspan所有的element都釋放了衩椒,那么使用freeSpan()
- mheap.freeSpan()釋放golang runtime的這個(gè)mspan對(duì)象蚌父,同時(shí)將mspan表示的地址空間標(biāo)記為可分配。
地址空間回收(scavenge)
如上節(jié)所述毛萌,sweep只是將page標(biāo)記為可分配苟弛,但是并未把地址空間釋放;真正的地址空間釋放是scavenge過程阁将。
真正的scavenge是由pageAlloc.scavenge()—>sysUnused()將掃描到待釋放的chunk所表示的地址空間釋放掉(使用sysUnused()將地址空間還給os)
golang的scavenge過程有兩種:
- 同步scavenge膏秫,當(dāng)每次mheap.grow() 增長mheap的內(nèi)存的時(shí)刻,如果增長量達(dá)到一定水平就會(huì)觸發(fā)scavenge的掃描過程做盅。在scavenge掃描過程中缤削,golang會(huì)嘗試釋放一定數(shù)量的chunk。
- 后臺(tái)scavenge吹榴,golang內(nèi)部有定時(shí)器和相關(guān)的goroutine亭敢,定期掃描程序內(nèi)存使用量,當(dāng)內(nèi)存使用量超出一定閾值的時(shí)候图筹,也會(huì)調(diào)用scavenge過程帅刀,嘗試釋放內(nèi)存給os系統(tǒng)。
- golang runtime package中定義了 debug.freeOSMemory 會(huì)手動(dòng)觸發(fā)一次gc远剩,并調(diào)用scavengeAll扣溺,對(duì)mheap管理所有地址空間進(jìn)行scavenge *