這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
記憶體管理緩衝結構
Go實現的記憶體管理採用了tcmalloc這種架構,並配合goroutine和記憶體回收。tcmalloc的基本策略就是將記憶體分為多個層級。申請對象優先從最小層級的記憶體管理集合mcache中擷取,若mcache無法命中則需要向mcentral申請一批記憶體塊緩衝到本地mcache中,若mcentral無閒置記憶體塊,則向mheap申請來填充mcentral,最後向系統申請。
mcache + mspan
最小層級的記憶體塊管理集合mcache由goroutine自己維護,這樣從中申請記憶體不用加鎖。它是一個大小為67的數組,不同的index對應不同規格的mspan。newobject的時候通過sizetoclass計算對應的規格,然後在mcache中擷取mspan對象。
type mcache struct { alloc [_NumSizeClasses]*mspan // spans to allocate from}
mspan包含著一批大小相同的閒置object,由freelist指標尋找。mspan內部的object是連續記憶體塊,即連續的n個page(4KB)的連續記憶體空間。然後這塊空間被平均分成了規格相同的object,這些object又串連成鏈表。當newobject時找到mcache中對應規格的mspan,從它的freelist取一個object即可。
type mspan struct { next *mspan // in a span linked list prev *mspan // in a span linked list start pageID // starting page number npages uintptr // number of pages in span freelist gclinkptr // list of free objects sizeclass uint8 // size class incache bool // being used by an mcache}
mheap + mcentral
如果某個規格的span裡已經沒有freeObject了 需要從mcentral當中擷取這種規格的mspan。正好mcentral也是按照class規格儲存在數組中,只要按規格去mheap的mcentral數組取mspan就好。
// 某種規格的mspan正好對應一個mcentraltype mcentral struct { lock mutex sizeclass int32 nonempty mspan //還有空閑object的mspan empty mspan //沒有空閑object或已被cache取走的mspan}
如果central數組中這種規格的mcentral沒有freeSpan了,則需要從mheap的free數組擷取。這裡規格並不對齊,所以應該要重新切分成相應規格的mspan。
type mheap struct { lock mutex free [_MaxMHeapList]mspan // 頁數在127以內的空閑span鏈表 freelarge mspan spans **mspan bitmap uintptr bitmap_mapped uintptr arena_start uintptr arena_used uintptr arena_end uintptr arena_reserved bool central [_NumSizeClasses]struct { mcentral mcentral pad [_CacheLineSize]byte } spanalloc fixalloc // allocator for span* cachealloc fixalloc // allocator for mcache*}
記憶體的初始化
很早之前看過這個圖,當時對他的理解有誤,因為看漏了一句話 struct Mcache alloc from 'cachealloc' by FixAlloc。就是說使用者進程newobject是從的arena地區分配的,而runtime層自身管理的結構 比如mcache等是專門設計了fixAlloc來分配的,原因可能是這些runtime層的管理物件類型和長度都相對固定,而且生命週期很長,不適合佔用arena地區。
mallocinit
通過sysReserve 向系統申請一塊連續的記憶體 spans+bitmap+arena。其中arena為各個層級緩衝結構提供的分配的記憶體塊,spans是個指標數組用來按照page定址arena地區。
最終sysReserve調用的是系統調用mmap。申請了512GB的虛擬位址空間,真正的實體記憶體則是用到的時候發生缺頁才真實佔用的。
func mallocinit() { // 初始化規格class和size的對照方法 initSizes() if ptrSize == 8 && (limit == 0 || limit > 1<<30) { arenaSize := round(_MaxMem, _PageSize) bitmapSize = arenaSize / (ptrSize * 8 / 4) spansSize = arenaSize / _PageSize * ptrSize pSize = bitmapSize + spansSize + arenaSize + _PageSize p1 = uintptr(sysReserve(unsafe.Pointer(p), pSize, &reserved)) } mheap_.spans = (**mspan)(unsafe.Pointer(p1)) mheap_.bitmap = p1 + spansSize mheap_.arena_start = p1 + (spansSize + bitmapSize) mheap_.arena_used = mheap_.arena_start mheap_.arena_end = p + pSize mheap_.arena_reserved = reserved mHeap_Init(&mheap_, spansSize) _g_ := getg() _g_.m.mcache = allocmcache()}
mheap初始化相關指標,使之可以定址arena這塊記憶體。同時初始化cachealloc這個固定分配器。最後執行的 m.mcache = allocmcache() 是每個gouroutine建立時都要初始化的。直到這時才真正建立了mcache,並且初始化mcache裡整個數組對應的mspan為emptyspan。
func (h *mheap) init(spansStart, spansBytes uintptr) { h.spanalloc.init(unsafe.Sizeof(mspan{}), recordspan, unsafe.Pointer(h), &memstats.mspan_sys) h.cachealloc.init(unsafe.Sizeof(mcache{}), nil, nil, &memstats.mcache_sys) h.spanalloc.zero = false for i := range h.free { h.free[i].init() h.busy[i].init() } h.freelarge.init() h.busylarge.init() for i := range h.central { h.central[i].mcentral.init(int32(i)) } sp := (*slice)(unsafe.Pointer(&h.spans)) sp.array = unsafe.Pointer(spansStart) sp.len = 0 sp.cap = int(spansBytes / sys.PtrSize)}func allocmcache() *mcache { // lock and fixalloc mcache c := (*mcache)(mheap_.cachealloc.alloc()) for i := 0; i < _NumSizeClasses; i++ { c.alloc[i] = &emptymspan } return c}
fixalloc
fixalloc分配器通過init初始化每次分配的size。chunk是每次分配的固定大小的記憶體塊,list是記憶體塊鏈表。當fixalloc初始化為cachealloc時,每次調用alloc就分配一塊mcache。persistantalloc看起來是runtime有個全域儲存的後備記憶體的地方,優先從這兒取沒有再從系統mmap一塊。
type fixalloc struct { size uintptr first func(arg, p unsafe.Pointer) arg unsafe.Pointer list *mlink chunk unsafe.Pointer nchunk uint32 inuse uintptr // in-use bytes now stat *uint64 zero bool // zero allocations}func (f *fixalloc) alloc() unsafe.Pointer { // 優先從可複用鏈表中擷取對象塊 if f.list != nil { f.list = f.list.next return v } // 如果沒有從系統申請chunk大小的記憶體塊 if uintptr(f.nchunk) < f.size { f.chunk = persistentalloc(_FixAllocChunk, 0, f.stat) } v := f.chunk // 為調用方提供了fist函數作為hook點 return v}
記憶體配置
mallocgc
以下總結了malloc的流程,基本普通的小對象都是從mcache中找到相應規格的mspan,在其中的freelist上拿到object對象記憶體塊。nextfree中隱藏了整個記憶體資料區塊的尋找和流向。
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer { c := gomcache() if size <= maxSmallSize { // size小於16bit的不用掃描的對象 直接從mcache的tiny上分 if noscan && size < maxTinySize { off := c.tinyoffset if off+size <= maxTinySize && c.tiny != 0 { x = unsafe.Pointer(c.tiny + off) return x } // 若沒有tiny了則從mcache的中相應規格的mspan尋找 span := c.alloc[tinySizeClass] v, _, shouldhelpgc = c.nextFree(tinySizeClass) x = unsafe.Pointer(v) } else { // 普通小於4KB小對象先計算規格 span := c.alloc[sizeclass] v, span, shouldhelpgc = c.nextFree(sizeclass) } } else { // 大對象直接從heap分配span systemstack(func() { s = largeAlloc(size, needzero) }) x = unsafe.Pointer(s.base()) } return x}func (c *mcache) nextFree(sizeclass uint8) (v gclinkptr, s *mspan, shouldhelpgc bool) { s = c.alloc[sizeclass] freeIndex := s.nextFreeIndex() if freeIndex == s.nelems { systemstack(func() { c.refill(int32(sizeclass)) }) s = c.alloc[sizeclass] freeIndex = s.nextFreeIndex() } v = gclinkptr(freeIndex*s.elemsize + s.base()) return}
refill + cachespan
如果nextfree在mcache相應規格的mspan裡拿不到object那麼需要從mcentral中refill記憶體塊。
這裡面有個細節要將alloc中原本已經沒有可用object的這塊mspan還給central,應該要放進central的empty鏈表中。這裡只是把相應的mspan的incache設定為false,等待sweep的回收。
func (c *mcache) refill(sizeclass int32) *mspan { s := c.alloc[sizeclass] if s != &emptymspan { s.incache = false } s = mheap_.central[sizeclass].mcentral.cacheSpan() c.alloc[sizeclass] = s return s}
sweepgen是個回收標記,當sweepgen=sg-2時表示等待回收,sweepgen-1表示正在回收,sweepgen表示已經回收。從mcentral中擷取mspan時有可能當前的span正在等待或正在回收,我們把等待回收的mspan可以返回用來refill mcache,因此將它insert到empty鏈表中。
func (c *mcentral) cacheSpan() *mspan { sg := mheap_.sweepgenretry: var s *mspan for s = c.nonempty.first; s != nil; s = s.next { if s.sweepgen == sg-2 && atomic.Cas(&s.sweepgen, sg-2, sg-1) { // 等待回收 可以返回使用 c.nonempty.remove(s) c.empty.insertBack(s) s.sweep(true) goto havespan } if s.sweepgen == sg-1 { // 正在回收 忽略 continue } c.nonempty.remove(s) c.empty.insertBack(s) goto havespan } for s = c.empty.first; s != nil; s = s.next {...} s = c.grow() c.empty.insertBack(s)havespan: ... return s}
mcentral grow
如果mcentral中沒有mspan可以用 那麼需要grow,即從mheap中擷取。要計算出當前規格對應的page數目,從mheap中直接去nPage的mspan。free地區是個指標數組,每個指標對應一個mspan的鏈表,數組按照npage定址。若大於要求的npage的鏈表中 都沒有空閑mspan,則mheap也需要擴張。
func (c *mcentral) grow() *mspan { npages := uintptr(class_to_allocnpages[c.sizeclass]) size := uintptr(class_to_size[c.sizeclass]) n := (npages << _PageShift) / size s := mheap_.alloc(npages, c.sizeclass, false, true) heapBitsForSpan(s.base()).initSpan(s) return s}func (h *mheap) allocSpanLocked(npage uintptr) *mspan { for i := int(npage); i < len(h.free); i++ { list = &h.free[i] if !list.isEmpty() { s = list.first goto HaveSpan } } list = &h.freelarge s = h.allocLarge(npage) if s == nil { if !h.grow(npage) { return nil } s = h.allocLarge(npage) }HaveSpan: // Mark span in use. return s}
mheap grow
mheap的擴張h.sysAlloc直接向arena地區申請nbytes的記憶體,數目按照npage大小計算。arena地區的一些指標標記開始移動,最終將mspan加入鏈表,等待分配。
func (h *mheap) grow(npage uintptr) bool { ask := npage << _PageShift v := h.sysAlloc(ask) s := (*mspan)(h.spanalloc.alloc()) s.init(uintptr(v), ask>>_PageShift) p := (s.base() - h.arena_start) >> _PageShift for i := p; i < p+s.npages; i++ { h.spans[i] = s } atomic.Store(&s.sweepgen, h.sweepgen) s.state = _MSpanInUse h.pagesInUse += uint64(s.npages) // 加入鏈表 h.freeSpanLocked(s, false, true, 0) return true}
記憶體回收與釋放
簡單說兩句:mspan裡有sweepgen回收標記,回收的記憶體會先全部回到mcentral。如果已經回收所有的mspan那麼可以返還給mheap的freelist。回收的記憶體塊當然是為了複用,並不直接釋放。
func (s *mspan) sweep(preserve bool) bool { res = mheap_.central[cl].mcentral.freeSpan(s, preserve, wasempty)}func (c *mcentral) freeSpan(s *mspan, preserve bool, wasempty bool) bool { if wasempty { c.empty.remove(s) c.nonempty.insert(s) } ... c.nonempty.remove(s) mheap_.freeSpan(s, 0) return true}
監控線程sysmon又出現了,它會遍曆mheap中所有的free freelarge裡的mspan,發現空閑時間超過閾值就madvise建議核心釋放它相關的實體記憶體。