探索Go記憶體管理(分配)

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

基於1.8.3版本,64位Linux作業系統

1、概述

Go記憶體管理基於 tcmalloc,使用連續虛擬位址,以頁(8k)為單位、多級緩衝進行管理;

在分配記憶體時,需要對size進行對齊處理,根據best-fit找到合適的mspan,對未用完的記憶體還會拆分成其他大小的mspan繼續使用

在new一個object時(忽略逃逸分析),根據object的size做不同的分配策略:

  • 極小對象(size<16byte)直接在當前P的mcache上的tiny緩衝上分配;
  • 小對象(16byte <= size <= 32k)在當前P的mcache上對應slot的空閑列表中分配,無空閑列表則會繼續向mcentral申請(還是沒有則向mheap申請);
  • 大對象(size>32k)直接通過mheap申請。

2、資料結構

2.1 mspan

mspan並不直接擁有記憶體空間,它負責管理起始地址為startAddr、層級(預分配頁個數)為sizeclass的連續地址空間。

摘取重點內容(下同)type mspan struct {    //雙向鏈表    next *mspan         prev *mspan        //起始地址    startAddr     uintptr       //包含多少頁    npages        uintptr       //    stackfreelist gclinkptr     //有多少對象    nelems uintptr     //gc相關    sweepgen    uint32    //層級    sizeclass   uint8          //已被mcache使用    incache     bool          //狀態    state       mSpanState}

2.2 mcache

Go為Per-thread (in Go, per-P)分配了mcache管理結構,所以對其操作是不需要鎖的,每個mcache有大小為67的mspan數組,儲存不同層級大小的mspan

type mcache struct {    tiny             uintptr    tinyoffset       uintptr    alloc [_NumSizeClasses]*mspan    stackcache [_NumStackOrders]stackfreelist    ...}

2.3 mcentral

mcentral集中管理,當在mcache申請失敗的時候,會向mcentral申請;mcentral有個關鍵方法cacheSpan(),它是整個分配的核心演算法

type mcentral struct {    lock      mutex    sizeclass int32    nonempty  mSpanList     empty     mSpanList }

2.4 mheap

mheap是真實擁有虛擬位址的結構,同時擁有67個層級的mcentral,以及所有分配的mspan。

// _NumSizeClasses := 67// _MaxMHeapList := 128type mheap struct {    lock      mutex    //size < 128 * 8k(1M)的可用mspanList    free      [_MaxMHeapList]mSpanList     //size >= 128 * 8k(1M)的可用mspanList    freelarge mSpanList     busy      [_MaxMHeapList]mSpanList     busylarge mSpanList               //gc相關    sweepgen  uint32                       sweepdone uint32                    //所有的mspan    allspans []*mspan     //頁到span的尋找表    spans []*mspan    //位元影像    bitmap         uintptr     bitmap_mapped  uintptr    //真實申請的記憶體起始地址    arena_start    uintptr    //真實申請的記憶體目前可用起始地址    arena_used     uintptr     //真實申請的記憶體結束位址    arena_end      uintptr    //分級的mcentral    central [_NumSizeClasses]struct {        mcentral mcentral        pad      [sys.CacheLineSize]byte    }    ...}

2.5 四者的關係示圖


關係示圖

3、初始化

初始化時,Go向系統申請預留一段連續虛擬位址,大小(64位機器上)為512M(spans_mapped)+16G(bitmap_mapped)+512G(arena)

向系統申請預留的連續地址空間+----------+-----------+-----------------------------+|  spans   |   bitmap  |           arena             ||  512M    |     16G   |            512G             |+----------+-----------+-----------------------------+

mheap的初始化在func mallocinit()中,而mallocinit被schedinit()調用
/src/runtime/proc.go

// The bootstrap sequence is:////  call osinit//  call schedinit//  make & queue new G//  call runtime·mstart//// The new G calls runtime·main.

mallocinit的邏輯為:

func mallocinit() {        // 0. 檢查系統/硬體資訊,bala bala        // 1. 計算預留空間大小        arenaSize := round(_MaxMem, _PageSize)        bitmapSize = arenaSize / (sys.PtrSize * 8 / 2)        spansSize = arenaSize / _PageSize * sys.PtrSize        spansSize = round(spansSize, _PageSize)        // 2. 嘗試預留地址(區分不同平台 略)        for i := 0; i <= 0x7f; i++ {            ...            pSize = bitmapSize + spansSize + arenaSize + _PageSize            p = uintptr(sysReserve(unsafe.Pointer(p), pSize, &reserved))        }        // 3. 初始化部分mheap中變數        p1 := round(p, _PageSize)        spansStart := p1        mheap_.bitmap = p1 + spansSize + bitmapSize        mheap_.arena_start = p1 + (spansSize + bitmapSize)        mheap_.arena_end = p + pSize        mheap_.arena_used = p1 + (spansSize + bitmapSize)        mheap_.arena_reserved = reserved              // 4. 其他部分初始化,67個mcentral在這裡初始化        mheap_.init(spansStart, spansSize)        _g_ := getg()        _g_.m.mcache = allocmcache()}

mheap的初始化方法

// Initialize the heap.func (h *mheap) init(spansStart, spansBytes uintptr) {    // 0. xxalloc.init        // 1. free、busy init    for i := range h.free {        h.free[i].init()        h.busy[i].init()    }    h.freelarge.init()    h.busylarge.init()    // 2. mcentral初始化    for i := range h.central {        h.central[i].mcentral.init(int32(i))    }        // 3. spans初始化    sp := (*slice)(unsafe.Pointer(&h.spans))    sp.array = unsafe.Pointer(spansStart)    sp.len = 0    sp.cap = int(spansBytes / sys.PtrSize)}

mcentral的初始化比較簡單,設定自己的層級,同時將兩個mspanList初始化

而mcache的初始化在func procresize(nprocs int32) *p中,procresize也在schedinit()中調用,順序在mallocinit()之後,也就是說發生在mheap於mcentral的初始化後面

func procresize(nprocs int32) *p {    // 0. bala bala    // 1. 初始化P    for i := int32(0); i < nprocs; i++ {        pp := allp[i]        //初始化每個P的mcache        if pp.mcache == nil {            if old == 0 && i == 0 {                if getg().m.mcache == nil {                    throw("missing mcache?")                }                pp.mcache = getg().m.mcache             } else {                pp.mcache = allocmcache()            }        }    }}

而allocmcache比較簡單

func allocmcache() *mcache {    lock(&mheap_.lock)    c := (*mcache)(mheap_.cachealloc.alloc())    unlock(&mheap_.lock)    for i := 0; i < _NumSizeClasses; i++ {        c.alloc[i] = &emptymspan    }    c.next_sample = nextSample()    return c}

至此,管理結構mheap、67個mcentral及每個P的mcache都初始化完畢,接下來進入重點--分配階段。

4、分配

前面說過,在指派至記憶體時,根據對象的大小分為3個層級:極小、小、大;在這裡我們假設關閉內聯最佳化,即沒有逃逸的存在。當new一個對象時,調用的是:

func newobject(typ *_type) unsafe.Pointer {    return mallocgc(typ.size, typ, true)}
mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {        dataSize := size    c := gomcache()    var x unsafe.Pointer    noscan := typ == nil || typ.kind&kindNoPointers != 0    if size <= maxSmallSize {        if noscan && size < maxTinySize {            // 極小對象        } else {            // 小對象    } else {        // 大對象    }}

我們將針對這三類一一分析

  • 首先是極小對象(<16byte)
off := c.tinyoffset// 地址對齊if size&7 == 0 {    off = round(off, 8)} else if size&3 == 0 {    off = round(off, 4)} else if size&1 == 0 {    off = round(off, 2)}//若之前tiny剩餘空間夠用,則將極小對象拼在一起if off+size <= maxTinySize && c.tiny != 0 {    // The object fits into existing tiny block.    x = unsafe.Pointer(c.tiny + off)    c.tinyoffset = off + size    c.local_tinyallocs++    mp.mallocing = 0    releasem(mp)    return x}//不若,則申請新的mspan// Allocate a new maxTinySize block.span := c.alloc[tinySizeClass]v := nextFreeFast(span)if v == 0 {    v, _, shouldhelpgc = c.nextFree(tinySizeClass)}x = unsafe.Pointer(v)(*[2]uint64)(x)[0] = 0(*[2]uint64)(x)[1] = 0// 新申請的剩餘空間大於之前的剩餘空間if size < c.tinyoffset || c.tiny == 0 {    c.tiny = uintptr(x)    c.tinyoffset = size}size = maxTinySize

其中nextFreeFast和nextFree先跳過去,因為小對象分配時也會使用到,之後一併分析;下面是極小對象分配的
先是有足夠剩餘空間,那麼對齊都直接利用(為了便於說明問題,tinyoffset用箭頭指向表示)



剩餘空間夠用

如果沒有足夠空間,則申請新的,若必要修正tiny及tinyoffset的值



剩餘空間不足
  • 接著分析小對象(16byte <= size <= 32k)
    介於16b到32k之間大小的對象分配比較複雜,可以結合文末的流程圖,便於記憶
var sizeclass uint8if size <= smallSizeMax-8 {    sizeclass = size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]} else {    sizeclass = size_to_class128[(size-smallSizeMax+largeSizeDiv-1)/largeSizeDiv]}size = uintptr(class_to_size[sizeclass])span := c.alloc[sizeclass]v := nextFreeFast(span)if v == 0 {    v, span, shouldhelpgc = c.nextFree(sizeclass)}x = unsafe.Pointer(v)if needzero && span.needzero != 0 {    memclrNoHeapPointers(unsafe.Pointer(v), size)}                                          

首先計算申請對象的sizeclass,以此找到對應大小的mspan;然後找到可用的地址。這裡面有兩個重要的方法nextFreeFast和nextFree:

// nextFreeFast returns the next free object if one is quickly available.// Otherwise it returns 0.func nextFreeFast(s *mspan) gclinkptr {    //計算s.allocCache從低位起有多少個0    theBit := sys.Ctz64(s.allocCache)     if theBit < 64 {        result := s.freeindex + uintptr(theBit)        if result < s.nelems {            freeidx := result + 1            if freeidx%64 == 0 && freeidx != s.nelems {                return 0            }            //更新位元影像、可用遊標            s.allocCache >>= (theBit + 1)            s.freeindex = freeidx            //根據result和s.elemsize起始地址計算v            v := gclinkptr(result*s.elemsize + s.base())            s.allocCount++            return v        }    }    return 0}

重點是當mcache沒有可用地址時,通過nextFree向mcentral甚至mheap申請

func (c *mcache) nextFree(sizeclass uint8) (v gclinkptr, s *mspan, shouldhelpgc bool) {    s = c.alloc[sizeclass]    shouldhelpgc = false    freeIndex := s.nextFreeIndex()    if freeIndex == s.nelems {        // The span is full.        ...        //重新填充當前的mcache        systemstack(func() {            c.refill(int32(sizeclass))        })        shouldhelpgc = true        s = c.alloc[sizeclass]        freeIndex = s.nextFreeIndex()    }    ...    ...}

向mcentral是通過refill來實現的

func (c *mcache) refill(sizeclass int32) *mspan {    _g_ := getg()    _g_.m.locks++    // 想mcentral歸還當前的mspan    s := c.alloc[sizeclass]    if uintptr(s.allocCount) != s.nelems {        throw("refill of span with free space remaining")    }    if s != &emptymspan {        s.incache = false    }    // 擷取新的, mcentral.cacheSpan()重點分析    s = mheap_.central[sizeclass].mcentral.cacheSpan()    ...    c.alloc[sizeclass] = s    _g_.m.locks--    return s}

下面是一個很長的調用鏈路...

func (c *mcentral) cacheSpan() *mspan {    ...retry:    var s *mspan    //先從非空列表中找    for s = c.nonempty.first; s != nil; s = s.next {        ...        goto havespan    }    //沒有則從空列表中找    for s = c.empty.first; s != nil; s = s.next {           ...            goto retry        }    //實在沒有,那麼申請吧            s = c.grow()    if s == nil {        return nil    }    havespan:      //    return s}// 由mcentral申請func (c *mcentral) grow() *mspan {    ...    s := mheap_.alloc(npages, c.sizeclass, false, true)    ...    return s}//由mheap申請func (h *mheap) alloc(npage uintptr, sizeclass int32, large bool, needzero bool) *mspan {    ...    systemstack(func() {        s = h.alloc_m(npage, sizeclass, large)    })    ...    return s}func (h *mheap) alloc_m(npage uintptr, sizeclass int32, large bool) *mspan {    ...    s := h.allocSpanLocked(npage)    ...    return s}//Best-fit演算法func (h *mheap) allocSpanLocked(npage uintptr) *mspan {    //先從128頁以內(1M)的free列表中尋找    for i := int(npage); i < len(h.free); i++ {        list = &h.free[i]        ...    }    // Best-fit 對於大對象申請也會用到這個方法    //基本思路是找到最小可以滿足需求的mspan,如果有多個,選擇地址最小的    list = &h.freelarge    s = h.allocLarge(npage)    if s == nil {        //如果mheap也沒有空間了,向系統申請吧        if !h.grow(npage) {            return nil        }        s = h.allocLarge(npage)        if s == nil {            return nil        }    }HaveSpan:    //轉移s    list.remove(s)    if s.inList() {        throw("still in list")    }    //對於申請到的記憶體大於想要的,將其拆分,避免浪費    if s.npages > npage {        ...        h.freeSpanLocked(t, false, false, s.unusedsince)        s.state = _MSpanFree    }        return s}//向系統申請空間func (h *mheap) grow(npage uintptr) bool {    //計算頁數    npage = round(npage, (64<<10)/_PageSize)    ask := npage << _PageShift    if ask < _HeapAllocChunk {        ask = _HeapAllocChunk    }    v := h.sysAlloc(ask)    ...}func (h *mheap) sysAlloc(n uintptr) unsafe.Pointer {  ...  //    sysReserve調用mmap預留空間,至此調用鏈結束  p := uintptr(sysReserve(unsafe.Pointer(h.arena_end), p_size, &reserved))  ...           }
  • 最後是大對象
var s *mspanshouldhelpgc = truesystemstack(func() {    // largeAlloc會調用mheap_.alloc,這個方法在小對象申請時已經追蹤過    s = largeAlloc(size, needzero)})s.freeindex = 1s.allocCount = 1x = unsafe.Pointer(s.base())size = s.elemsize
  • 記憶體配置的流程圖



    流程圖

5、參考文獻

[1]. https://github.com/qyuhen
[2]. http://legendtkl.com/2017/04/02/golang-alloc/
[3]. https://tracymacding.gitbooks.io/implementation-of-golang/content/memory/memory_core_data_structure.html

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.