Go 記憶體管理

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

記憶體管理緩衝結構

Go實現的記憶體管理採用了tcmalloc這種架構,並配合goroutine和記憶體回收。tcmalloc的基本策略就是將記憶體分為多個層級。申請對象優先從最小層級的記憶體管理集合mcache中擷取,若mcache無法命中則需要向mcentral申請一批記憶體塊緩衝到本地mcache中,若mcentral無閒置記憶體塊,則向mheap申請來填充mcentral,最後向系統申請。

mcache + mspan

最小層級的記憶體塊管理集合mcache由goroutine自己維護,這樣從中申請記憶體不用加鎖。它是一個大小為67的數組,不同的index對應不同規格的mspannewobject的時候通過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了,則需要從mheapfree數組擷取。這裡規格並不對齊,所以應該要重新切分成相應規格的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建議核心釋放它相關的實體記憶體。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.