Golang---內存管理(內存分配)


  摘要:上次我們學習了 Golang 的 goroutine 調度策略,今天我們來學習 Golang 的內存管理策略。

思考內存管理如何設計

內存池

  最直接的方式是調用 malloc函數,指定要分配的大小,直接向操作系統申請。問題是這種方式會涉及到用戶態和內核態的切換過程,那么頻繁的切換就會帶來很大的性能下降,我們要減少用戶態和內核態的頻繁切換就需要自己申請一塊內存空間,將之分割成大小規格不同的內存塊來供程序使用,內存池是再適合不過的組成部分。

GC

  內存管理不光需要使用方便,還要保證內存使用過程能夠節約,畢竟整個系統的內存資源是有限的,那么就需要GC進行動態的垃圾回收,銷毀無用的對象,釋放內存來保證整個程序乃至系統運行平穩。

  一個應用程序內部之間存在大量的線程,線程之間資源是共享的,那么要保證同一塊內存使用過程不出現復用或者污染,就必須保證同一時間只能有一個線程進行申請,第一個想到的肯定是鎖,對公共區域的資源一定要加鎖,另一種方式就是內存隔離,這個在golang的mcache中會有體現。

 

基本概念

page

  操作系統內存管理中,內存的最粒度是4KB,也就是說分配內存最小4KB起。而golang里面一個page是8KB。

span

  span是golang內存管理的基本單位,每個span管理指定規格(以golang 中的 page為單位)的內存塊,內存池分配出不同規格的內存塊就是通過span體現出來的,應用程序創建對象就是通過找到對應規格的span來存儲的,下面是 mspan 結構中的主要部分。

//go\src\runtime\mheap.go
//go:notinheap
type mspan struct {
next *mspan     // next span in list, or nil if none
prev *mspan     // previous span in list, or nil if none

startAddr uintptr // address of first byte of span aka s.base()
npages    uintptr // number of pages in span

nelems uintptr // number of object in the span.

allocCache uint64
allocBits  *gcBits  //bitmap
gcmarkBits *gcBits  //bitmap

baseMask    uint16        // if non-0, elemsize is a power of 2, & this will get object allocation base
allocCount  uint16        // number of allocated objects
spanclass   spanClass     // size class and noscan (uint8)
}
mspan

  那么要想區分不同規格的span,我們必須要有一個標識,每個span通過spanclass標識屬於哪種規格的span,golang的span規格一共有67種,具體如下:

//from runtime.go\sizeclasses.go

// class  bytes/obj  bytes/span  objects  tail waste  max waste
//     1          8        8192     1024           0     87.50%
//     2         16        8192      512           0     43.75%
//     3         32        8192      256           0     46.88%
//     4         48        8192      170          32     31.52%
//     5         64        8192      128           0     23.44%
//     6         80        8192      102          32     19.07%
//     7         96        8192       85          32     15.95%
//     8        112        8192       73          16     13.56%
//     9        128        8192       64           0     11.72%
//    10        144        8192       56         128     11.82%
//    11        160        8192       51          32      9.73%
//    12        176        8192       46          96      9.59%
//    13        192        8192       42         128      9.25%
//    14        208        8192       39          80      8.12%
//    15        224        8192       36         128      8.15%
//    16        240        8192       34          32      6.62%
//    17        256        8192       32           0      5.86%
//    18        288        8192       28         128     12.16%
//    19        320        8192       25         192     11.80%
//    20        352        8192       23          96      9.88%
//    21        384        8192       21         128      9.51%
//    22        416        8192       19         288     10.71%
//    23        448        8192       18         128      8.37%
//    24        480        8192       17          32      6.82%
//    25        512        8192       16           0      6.05%
//    26        576        8192       14         128     12.33%
//    27        640        8192       12         512     15.48%
//    28        704        8192       11         448     13.93%
//    29        768        8192       10         512     13.94%
//    30        896        8192        9         128     15.52%
//    31       1024        8192        8           0     12.40%
//    32       1152        8192        7         128     12.41%
//    33       1280        8192        6         512     15.55%
//    34       1408       16384       11         896     14.00%
//    35       1536        8192        5         512     14.00%
//    36       1792       16384        9         256     15.57%
//    37       2048        8192        4           0     12.45%
//    38       2304       16384        7         256     12.46%
//    39       2688        8192        3         128     15.59%
//    40       3072       24576        8           0     12.47%
//    41       3200       16384        5         384      6.22%
//    42       3456       24576        7         384      8.83%
//    43       4096        8192        2           0     15.60%
//    44       4864       24576        5         256     16.65%
//    45       5376       16384        3         256     10.92%
//    46       6144       24576        4           0     12.48%
//    47       6528       32768        5         128      6.23%
//    48       6784       40960        6         256      4.36%
//    49       6912       49152        7         768      3.37%
//    50       8192        8192        1           0     15.61%
//    51       9472       57344        6         512     14.28%
//    52       9728       49152        5         512      3.64%
//    53      10240       40960        4           0      4.99%
//    54      10880       32768        3         128      6.24%
//    55      12288       24576        2           0     11.45%
//    56      13568       40960        3         256      9.99%
//    57      14336       57344        4           0      5.35%
//    58      16384       16384        1           0     12.49%
//    59      18432       73728        4           0     11.11%
//    60      19072       57344        3         128      3.57%
//    61      20480       40960        2           0      6.87%
//    62      21760       65536        3         256      6.25%
//    63      24576       24576        1           0     11.45%
//    64      27264       81920        3         128     10.00%
//    65      28672       57344        2           0      4.91%
//    66      32768       32768        1           0     12.50%
spanclass

  另外上表可見最大的對象是32KB大小,超過32KB大小的由特殊的class表示,該class ID為0,每個class只包含一個對象。所以上面只有列出了1-66。

  下面還要三個數組,分別是:class_to_sizesize_to_classclass_to_allocnpages3個數組,對應下圖上的3個箭頭:

 

 比如:我們只拿第一行舉例:

// class  bytes/obj  bytes/span  objects  tail waste  max waste
//     1          8        8192     1024           0     87.50%

就是類別1的對象大小是8bytes,所以class_to_size[1]=8;span大小是8KB,為1頁,所以class_to_allocnpages[1]=1

mcache

  mcache保存的是各種大小的Span,並按Span class分類,小對象(<=32KB)直接從mcache分配內存,它起到了緩存的作用,並且可以無鎖訪問mcache是每個邏輯處理器(P)的本地內存線程緩存。Go中是每個P只擁有1個mcache,所以不用加鎖。另外,mcache中每個級別的Span有2類數組鏈表,但是合在一起的(alloc成員變量)。

//from runtime.go\mcache.go

type mcache struct {
local_scan  uintptr // bytes of scannable heap allocated

tiny             uintptr
tinyoffset       uintptr
local_tinyallocs uintptr // number of tiny allocs not counted in other stats

// The rest is not accessed on every malloc.

alloc [numSpanClasses]*mspan // numSpanClasses 為 2*67

stackcache [_NumStackOrders]stackfreelist  //每個 G 綁定的棧空間

}
mcache

mcentral

  它按Span class對Span分類,串聯成鏈表,當mcache的某個級別Span的內存被分配光時,它會向mcentral申請1個當前級別的Span。所有線程共享的緩存,需要加鎖訪問。

//from runtime.go\mcentral.go

type mcentral struct {
    lock      mutex
    spanclass spanClass

    // For !go115NewMCentralImpl.
    nonempty mSpanList // list of spans with a free object, ie a nonempty free list
    empty    mSpanList // list of spans with no free objects (or cached in an mcache)

    partial [2]spanSet // list of spans with a free object
    full    [2]spanSet // list of spans with no free objects
    
    nmalloc uint64
}
mcentral

  每個mcentral包含兩個mspanList

  • empty:雙向span鏈表,包括沒有空閑對象的span或緩存mcache中的span。當此處的span被釋放時,它將被移至non-empty span鏈表。
  • non-empty:有空閑對象的span雙向鏈表。當從mcentral請求新的span,mcentral將從該鏈表中獲取span並將其移入empty span鏈表。

mheap

  它把從OS申請出的內存頁組織成Span,並保存起來。當mcentral的Span不夠用時會向mheap申請,mheap的Span不夠用時會向OS申請,向OS的內存申請是按頁來的,然后把申請來的內存頁生成Span組織起來,同樣也是需要加鎖訪問的。大對象(>32KB)直接從mheap上分配。

//from runtime.go\mheap.go

type mheap struct {
// lock must only be acquired on the system stack, otherwise a g
// could self-deadlock if its stack grows with the lock held.
lock      mutex
pages     pageAlloc // page allocation data structure
sweepgen  uint32    // sweep generation, see comment in mspan; written during STW
sweepdone uint32    // all spans are swept
sweepers  uint32    // number of active sweepone calls


allspans []*mspan // all spans out there
}
mheap.go

  mhead 的結構相對比較復雜,我們知道每個golang程序啟動時候會向操作系統申請一塊虛擬內存空間,僅僅是虛擬內存空間,真正需要的時候才會發生缺頁中斷,向系統申請真正的物理空間,在golang1.11版本以后,申請的內存空間會放在一個heapArena數組里,由arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena表示,用於應用程序內存分配,根據源碼公式,在64位非windows系統分配大小是64MB,windows 64位是4MB。

內存分配過程

分配過程總覽圖

內存關系總覽圖

 

 

簡單分配規則

  • tiny對象內存分配,直接向mcache的tiny對象分配器申請,如果空間不足,則向mcache的tinySpanClass規格的span鏈表申請,如果沒有,則向mcentral申請對應規格mspan,依舊沒有,則向mheap申請,最后都用光則向操作系統申請。

  • 小對象內存分配,先向本線程mcache申請,發現mspan沒有空閑的空間,向mcentral申請對應規格的mspan,如果mcentral對應規格沒有,向mheap申請對應頁初始化新的mspan,如果也沒有,則向操作系統申請,分配頁。

  • 大對象內存分配,直接向mheap申請spanclass=0,如果沒有則向操作系統申請。

  源碼分析

對象分配入口

Tiny 對象(<16B)的分配: golang會通過tiny和tinyoffset組合尋找位置分配內存空間,這樣可以更好的節約空間

//from runtime.go\malloc.go

//Tiny 對象的分配過程
//step1:先進行內存對齊
off := c.tinyoffset
// Align tiny pointer for required (conservative) alignment.
if size&7 == 0 {
    off = alignUp(off, 8)
} else if size&3 == 0 {
    off = alignUp(off, 4)
} else if size&1 == 0 {
    off = alignUp(off, 2)
}
//step2: 看 tinySpanClass 是否還可以放下當前的 Tiny 對象,如果放不下,再申請一個類型為 tinySpanClass&&noscan 的 span
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
}else {
    // otherwise Allocate a new maxTinySize block.
    span = c.alloc[tinySpanClass]
}
alloc Tiny Object

小對象[16B, 32KB]的分配:會使用這部分span進行正常的內存分配

//from runtime.go\malloc.go

var sizeclass uint8
//step1: 確定規格sizeClass
if size <= smallSizeMax-8 {
    sizeclass = size_to_class8[divRoundUp(size, smallSizeDiv)]
} else {
    sizeclass = size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)]
}
size = uintptr(class_to_size[sizeclass])
spc := makeSpanClass(sizeclass, noscan)
//step2: 分配對應spanClass 的 span
span = c.alloc[spc]
v := nextFreeFast(span)
if v == 0 {
    v, span, shouldhelpgc = c.nextFree(spc)
}
x = unsafe.Pointer(v)
if needzero && span.needzero != 0 {
    memclrNoHeapPointers(unsafe.Pointer(v), size)
}
alloc smallObject

大對象(>32KB)的分配:直接在 mheap 上進行分配

//from runtime.go\malloc.go

shouldhelpgc = true
systemstack(func() {
    //分配大對象
    span = largeAlloc(size, needzero, noscan)
})
span.freeindex = 1
span.allocCount = 1
x = unsafe.Pointer(span.base())
size = span.elemsize
alloc bigObject

向上級申請資源

mcache 向 mcentral 申請: 調用 \src\runtime\mcache.go refill 方法

func (c *mcache) refill(spc spanClass) {
    // Return the current cached span to the central lists.
    s := c.alloc[spc]
    
    if uintptr(s.allocCount) != s.nelems {
    throw("refill of span with free space remaining")
    }
    
    // Get a new cached span from the central lists.
    //step1: 從 mcentral 獲取資源
    s = mheap_.central[spc].mcentral.cacheSpan()
    if s == nil {
    throw("out of memory")
    }
    
    if uintptr(s.allocCount) == s.nelems {
    throw("span has no free space")
    }
    
    // Indicate that this span is cached and prevent asynchronous
    // sweeping in the next sweep phase.
    s.sweepgen = mheap_.sweepgen + 3
    //step2: 放入mcache 中
    c.alloc[spc] = s
}
mcache apply source

mcentral 向 mheap 申請: 調用 \src\runtime\mcental.go grow方法

// grow allocates a new empty span from the heap and initializes it for c's size class.
func (c *mcentral) grow() *mspan {
    npages := uintptr(class_to_allocnpages[c.spanclass.sizeclass()])
    size := uintptr(class_to_size[c.spanclass.sizeclass()])

    s := mheap_.alloc(npages, c.spanclass, true)
    if s == nil {
        return nil
    }

    // Use division by multiplication and shifts to quickly compute:
    // n := (npages << _PageShift) / size
    n := (npages << _PageShift) >> s.divShift * uintptr(s.divMul) >> s.divShift2
    s.limit = s.base() + size*n
    heapBitsForAddr(s.base()).initSpan(s)
    return s
}
mcentral apply source

mheap 向 os 申請: 調用\src\runtime\mheap.go grow方法

// Try to add at least npage pages of memory to the heap,
// returning whether it worked.
//
// h must be locked.
func (h *mheap) grow(npage uintptr) bool {
    // We must grow the heap in whole palloc chunks.
    ask := alignUp(npage, pallocChunkPages) * pageSize

    totalGrowth := uintptr(0)
    // This may overflow because ask could be very large
    // and is otherwise unrelated to h.curArena.base.
    end := h.curArena.base + ask
    nBase := alignUp(end, physPageSize)
    if nBase > h.curArena.end || /* overflow */ end < h.curArena.base {
        // Not enough room in the current arena. Allocate more
        // arena space. This may not be contiguous with the
        // current arena, so we have to request the full ask.
        av, asize := h.sysAlloc(ask)
        if av == nil {
            print("runtime: out of memory: cannot allocate ", ask, "-byte block (", memstats.heap_sys, " in use)\n")
            return false
        }

        if uintptr(av) == h.curArena.end {
            // The new space is contiguous with the old
            // space, so just extend the current space.
            h.curArena.end = uintptr(av) + asize
        } else {
            // The new space is discontiguous. Track what
            // remains of the current space and switch to
            // the new space. This should be rare.
            if size := h.curArena.end - h.curArena.base; size != 0 {
                h.pages.grow(h.curArena.base, size)
                totalGrowth += size
            }
            // Switch to the new space.
            h.curArena.base = uintptr(av)
            h.curArena.end = uintptr(av) + asize
        }

        // The memory just allocated counts as both released
        // and idle, even though it's not yet backed by spans.
        //
        // The allocation is always aligned to the heap arena
        // size which is always > physPageSize, so its safe to
        // just add directly to heap_released.
        mSysStatInc(&memstats.heap_released, asize)
        mSysStatInc(&memstats.heap_idle, asize)

        // Recalculate nBase.
        // We know this won't overflow, because sysAlloc returned
        // a valid region starting at h.curArena.base which is at
        // least ask bytes in size.
        nBase = alignUp(h.curArena.base+ask, physPageSize)
    }

    // Grow into the current arena.
    v := h.curArena.base
    h.curArena.base = nBase
    h.pages.grow(v, nBase-v)
    totalGrowth += nBase - v

    // We just caused a heap growth, so scavenge down what will soon be used.
    // By scavenging inline we deal with the failure to allocate out of
    // memory fragments by scavenging the memory fragments that are least
    // likely to be re-used.
    if retained := heapRetained(); retained+uint64(totalGrowth) > h.scavengeGoal {
        todo := totalGrowth
        if overage := uintptr(retained + uint64(totalGrowth) - h.scavengeGoal); todo > overage {
            todo = overage
        }
        h.pages.scavenge(todo, false)
    }
    return true
}
mheap apply source

 

參考資料:

https://www.cnblogs.com/xiaoxlm/p/12587557.html

https://www.cnblogs.com/33debug/p/12068699.html

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM