Skip to content

memory

Khác với c/c++ truyền thống, go là một ngôn ngữ có gc, trong hầu hết các trường hợp việc phân phối và giải phóng bộ nhớ do go tự động quản lý, bộ nhớ của một đối tượng nên được phân phối trên stack hay heap do compiler quyết định, về cơ bản không cần người dùng tham gia vào quản lý bộ nhớ, việc người dùng cần làm chỉ là sử dụng bộ nhớ. Trong go, quản lý bộ nhớ heap chủ yếu có hai component lớn, bộ phân phối bộ nhớ负责 phân phối bộ nhớ heap, bộ thu gom rác负责 thu hồi giải phóng bộ nhớ vô dụng, bài viết này chủ yếu讲的是 cách làm việc của bộ phân phối bộ nhớ, bộ phân phối bộ nhớ của go chịu ảnh hưởng rất lớn từ bộ phân phối bộ nhớ TCMalloc của google.

Bộ phân phối

Trong go có hai loại bộ phân phối bộ nhớ, một loại là bộ phân phối tuyến tính, loại khác là bộ phân phối chuỗi.

Phân phối tuyến tính

Bộ phân phối tuyến tính tương ứng với cấu trúc runtime.linearAlloc, như dưới đây

go
type linearAlloc struct {
  next   uintptr // next free byte
  mapped uintptr // one byte past end of mapped space
  end    uintptr // end of reserved space

  mapMemory bool // transition memory from Reserved to Ready if true
}

Bộ phân phối này sẽ xin cấp từ hệ điều hành một không gian bộ nhớ liên tục, next trỏ đến địa chỉ bộ nhớ có thể sử dụng, end trỏ đến địa chỉ cuối của không gian bộ nhớ, đại khái có thể hiểu như hình dưới.

Cách phân phối bộ nhớ của bộ phân phối tuyến tính rất dễ hiểu, dựa vào kích thước bộ nhớ xin cấp để kiểm tra xem có đủ không gian dư để chứa không, nếu đủ thì cập nhật trường next và trả về địa chỉ bắt đầu của không gian dư, code như sau.

go
func (l *linearAlloc) alloc(size, align uintptr, sysStat *sysMemStat) unsafe.Pointer {
  p := alignUp(l.next, align)
  if p+size > l.end {
    return nil
  }
  l.next = p + size
  return unsafe.Pointer(p)
}

Ưu điểm của cách phân phối này là nhanh và đơn giản, nhược điểm cũng khá rõ ràng, là không thể tái sử dụng bộ nhớ đã giải phóng, vì trường next chỉ trỏ đến địa chỉ bộ nhớ không gian dư, đối với không gian bộ nhớ đã sử dụng rồi giải phóng trước đó thì không thể cảm nhận được, làm như vậy sẽ gây lãng phí không gian bộ nhớ rất lớn, như hình dưới.

Nên phân phối tuyến tính không phải là cách phân phối chủ yếu trong go, nó chỉ được sử dụng như chức năng phân phối bộ nhớ trước trên máy 32 bit.

Phân phối chuỗi

Bộ phân phối chuỗi tương ứng với cấu trúc runtime.fixalloc, bộ nhớ mà bộ phân phối chuỗi phân phối không liên tục, tồn tại dưới dạng linked list đơn. Bộ phân phối chuỗi do若干 khối bộ nhớ cố định kích thước组成, mà mỗi khối bộ nhớ do若干片 bộ nhớ cố định kích thước组成, mỗi lần phân phối bộ nhớ, sẽ sử dụng một片 bộ nhớ cố định kích thước.

go
type fixalloc struct {
  size   uintptr
  first  func(arg, p unsafe.Pointer) // called first time p is returned
  arg    unsafe.Pointer
  list   *mlink
  chunk  uintptr // use uintptr instead of unsafe.Pointer to avoid write barriers
  nchunk uint32  // bytes remaining in current chunk
  nalloc uint32  // size of new chunks in bytes
  inuse  uintptr // in-use bytes now
  stat   *sysMemStat
  zero   bool // zero allocations
}

type mlink struct {
  _    sys.NotInHeap
  next *mlink
}

Trường của nó không đơn giản dễ hiểu như bộ phân phối tuyến tính, ở đây giới thiệu đơn giản một số trường quan trọng

  • size, chỉ mỗi lần phân phối bộ nhớ sử dụng bao nhiêu bộ nhớ.
  • list, trỏ đến node đầu của khối bộ nhớ có thể tái sử dụng, mỗi片 không gian bộ nhớ kích thước do size quyết định.
  • chunk, trỏ đến địa chỉ空闲 của khối bộ nhớ đang sử dụng hiện tại
  • nchunk, số byte khả dụng dư của khối bộ nhớ hiện tại
  • nalloc, kích thước khối bộ nhớ, cố định 16KB.
  • inuse,总共 đã sử dụng bao nhiêu byte bộ nhớ
  • zero, khi tái sử dụng khối bộ nhớ, có cần xóa bộ nhớ không

Bộ phân phối chuỗi giữ reference của khối bộ nhớ hiện tại và khối bộ nhớ có thể tái sử dụng, kích thước mỗi khối bộ nhớ đều cố định 16KB, giá trị này được thiết lập khi khởi tạo.

go
const _FixAllocChunk = 16 << 10

func (f *fixalloc) init(size uintptr, first func(arg, p unsafe.Pointer), arg unsafe.Pointer, stat *sysMemStat) {
  if size > _FixAllocChunk {
    throw("runtime: fixalloc size too large")
  }
  if min := unsafe.Sizeof(mlink{}); size < min {
    size = min
  }

  f.size = size
  f.first = first
  f.arg = arg
  f.list = nil
  f.chunk = 0
  f.nchunk = 0
  f.nalloc = uint32(_FixAllocChunk / size * size)
  f.inuse = 0
  f.stat = stat
  f.zero = true
}

Phân bố khối bộ nhớ như hình dưới, trong hình khối bộ nhớ được sắp xếp theo thứ tự thời gian tạo, thực tế địa chỉ của chúng không liên tục.

Kích thước bộ nhớ mà bộ phân phối chuỗi mỗi lần phân phối cũng cố định, do fixalloc.size quyết định, khi phân phối sẽ trước tiên kiểm tra xem có khối bộ nhớ có thể tái sử dụng không, nếu có thì ưu tiên sử dụng khối bộ nhớ tái sử dụng, rồi mới đi sử dụng khối bộ nhớ hiện tại, nếu không gian dư của khối bộ nhớ hiện tại không đủ để chứa thì sẽ tạo một khối bộ nhớ mới, phần logic này tương ứng với code dưới.

go
func (f *fixalloc) alloc() unsafe.Pointer {
  if f.size == 0 {
    print("runtime: use of FixAlloc_Alloc before FixAlloc_Init\n")
    throw("runtime: internal error")
  }

  if f.list != nil {
    v := unsafe.Pointer(f.list)
    f.list = f.list.next
    f.inuse += f.size
    if f.zero {
      memclrNoHeapPointers(v, f.size)
    }
    return v
  }
  if uintptr(f.nchunk) < f.size {
    f.chunk = uintptr(persistentalloc(uintptr(f.nalloc), 0, f.stat))
    f.nchunk = f.nalloc
  }

  v := unsafe.Pointer(f.chunk)
  if f.first != nil {
    f.first(f.arg, v)
  }
  f.chunk = f.chunk + f.size
  f.nchunk -= uint32(f.size)
  f.inuse += f.size
  return v
}

Ưu điểm của bộ phân phối chuỗi chính là nó có thể tái sử dụng bộ nhớ đã giải phóng, đơn vị cơ bản của việc tái sử dụng bộ nhớ là một片 bộ nhớ cố định kích thước, kích thước của nó do fixalloc.size quyết định, khi giải phóng bộ nhớ, bộ phân phối chuỗi sẽ lấy片 bộ nhớ này làm node đầu thêm vào linked list bộ nhớ空闲, code như dưới

go
func (f *fixalloc) free(p unsafe.Pointer) {
  f.inuse -= f.size
  v := (*mlink)(p)
  v.next = f.list
  f.list = v
}

Component bộ nhớ

Bộ phân phối bộ nhớ trong go chủ yếu do các component msapn, heaparena, mcache, mcentral, mheap cấu thành, chúng giữa tầng tầng tác dụng, quản lý toàn bộ bộ nhớ heap của go.

mspan

runtime.mspan là đơn vị cơ bản trong phân phối bộ nhớ go, cấu trúc của nó như sau

go
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
    freeindex uintptr

    spanclass             spanClass     // size class and noscan (uint8)
    needzero              uint8         // needs to be zeroed before allocation
    elemsize              uintptr       // computed from sizeclass or from npages
    limit                 uintptr       // end of data in span
    state                 mSpanStateBox // mSpanInUse etc; accessed atomically (get/set methods)

    nelems uintptr // number of object in the span.
    allocCache uint64
    allocCount            uint16        // number of allocated objects
    ...
}

Giữa mspanmspan liên kết dưới dạng doubly linked list thông qua nextprev, địa chỉ bộ nhớ không liên tục. Mỗi msapn quản lý mspan.npages trang bộ nhớ kích thước runtime.pageSize, thông thường kích thước trang là 8KB, và do mspan.startAddr ghi lại địa chỉ bắt đầu của các trang này và mspan.limit ghi lại địa chỉ cuối của bộ nhớ đã sử dụng. Kích thước elemsize của phần tử mà mỗi mspan lưu trữ là cố định, nên số lượng phần tử có thể chứa cũng cố định. Vì số lượng cố định, việc lưu trữ đối tượng giống như array phân bố trong mspan, phạm vi là [0, nelems], đồng thời do freeindex ghi lại index có thể dùng để lưu trữ đối tượng tiếp theo. mspan总共 có ba trạng thái

  • mSpanDead, bộ nhớ đã được giải phóng
  • mSpanInUse, được phân phối lên heap
  • mSpanManual, được phân phối vào phần dùng để quản lý bộ nhớ thủ công, ví dụ như stack.

Cái quyết định kích thước phần tử của mspanspanClass, spanClass tự nó là một số nguyên kiểu uint8, 7 bit cao lưu trữ giá trị class biểu thị 0-67, bit cuối cùng dùng để biểu thị noscan tức có chứa pointer không.

go
type spanClass uint8

func (sc spanClass) sizeclass() int8 {
  return int8(sc >> 1)
}

func (sc spanClass) noscan() bool {
  return sc&1 != 0
}

Nó总共 có 68 giá trị khác nhau, tất cả giá trị đều dưới dạng table lưu trữ trong file runtime.sizeclasses.go, trong runtime, sử dụng spanClass thông qua runtime.class_to_size có thể nhận được kích thước đối tượng của mspan, thông qua class_to_allocnpages có thể nhận được số trang của mspan.

classKích thước đối tượng tối đaKích thước spanSố lượng đối tượngLãng phí đuôiTỷ lệ lãng phí bộ nhớ tối đaCăn chỉnh tối thiểu
1881921024087.50%8
2168192512043.75%16
3248192341829.24%8
4328192256021.88%32
54881921703231.52%16
6648192128023.44%64
78081921023219.07%16
8968192853215.95%32
91128192731613.56%16
10128819264011.72%128
1114481925612811.82%16
12160819251329.73%32
13176819246969.59%16
141928192421289.25%64
15208819239808.12%16
162248192361288.15%32
17240819234326.62%16
1825681923205.86%256
1928881922812812.16%32
2032081922519211.80%64
21352819223969.88%32
223848192211289.51%128
2341681921928810.71%32
244488192181288.37%64
25480819217326.82%32
2651281921606.05%512
2757681921412812.33%64
2864081921251215.48%128
2970481921144813.93%64
3076881921051213.94%256
318968192912815.52%128
32102481928012.40%1024
3311528192712812.41%128
3412808192651215.55%256
351408163841189614.00%128
3615368192551214.00%512
37179216384925615.57%256
38204881924012.45%2048
39230416384725612.46%256
4026888192312815.59%128
413072245768012.47%1024
4232001638453846.22%128
4334562457673848.83%128
44409681922015.60%4096
45486424576525616.65%256
46537616384325610.92%256
476144245764012.48%2048
4865283276851286.23%128
4967844096062564.36%128
5069124915277683.37%256
51819281921015.61%8192
52947257344651214.28%256
5397284915255123.64%512
541024040960404.99%2048
55108803276831286.24%128
5612288245762011.45%4096
57135684096032569.99%256
581433657344405.35%2048
5916384163841012.49%8192
6018432737284011.11%2048
61190725734431283.57%128
622048040960206.87%4096
63217606553632566.25%256
6424576245761011.45%8192
652726481920312810.00%128
662867257344204.91%4096
6732768327681012.50%8192

Về logic tính toán của các giá trị này có thể tìm thấy trong hàm printComment của runtime.mksizeclasses.go, trong đó công thức tính tỷ lệ lãng phí bộ nhớ tối đa là

go
float64((size-prevSize-1)*objects+tailWaste) / float64(spanSize)

Ví dụ, khi class là 2, tỷ lệ lãng phí bộ nhớ tối đa của nó là

((16-8-1)*512+0)/8192 = 0.4375

Khi giá trị class là 0, chính là spanClass chuyên dùng để phân phối đối tượng lớn hơn 32KB, về cơ bản một đối tượng lớn sẽ chiếm một mspan. Nên, bộ nhớ heap của go thực tế là do若干 mspan链表 cố định kích thước khác nhau组成。

heaparena

前面 đã đề cập mspan là do若干 trang组成, nhưng mspan chỉ giữ reference địa chỉ của trang, không负责 quản lý các trang này, cái thực sự负责 quản lý các trang bộ nhớ này là runtime.heaparena. Mỗi heaparena quản lý若干 trang, kích thước của heaparena do runtime.heapArenaBytes quyết định, thông thường là 64MB. bitmap dùng để nhận biết xem địa chỉ trong trang có lưu trữ đối tượng không, zeroedBase chính là địa chỉ bắt đầu của trang bộ nhớ mà heaparena này quản lý, và do spans ghi lại mỗi trang do mspan nào sử dụng.

go
type heapArena struct {
  _ sys.NotInHeap
  bitmap [heapArenaBitmapWords]uintptr
  noMorePtrs [heapArenaBitmapWords / 8]uint8
  spans [pagesPerArena]*mspan
  pageInUse [pagesPerArena / 8]uint8
  pageMarks [pagesPerArena / 8]uint8
  pageSpecials [pagesPerArena / 8]uint8
  checkmarks *checkmarksMap
  zeroedBase uintptr
}

Về logic ghi lại trang và mspan có thể tìm thấy trong phương thức mheap.setSpans, như dưới

go
func (h *mheap) setSpans(base, npage uintptr, s *mspan) {
  p := base / pageSize
  ai := arenaIndex(base)
  ha := h.arenas[ai.l1()][ai.l2()]
  for n := uintptr(0); n < npage; n++ {
    i := (p + n) % pagesPerArena
    if i == 0 {
      ai = arenaIndex(base + n*pageSize)
      ha = h.arenas[ai.l1()][ai.l2()]
    }
    ha.spans[i] = s
  }
}

Trong heap của go, là do một mảng二维 heaparena để quản lý tất cả trang bộ nhớ, xem trường mheap.arenas.

go
type mheap struct {
  arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
}

Trên nền tảng windows 64 bit, mảng một chiều là 1 << 6, hai chiều là 1 << 16, trên nền tảng linux 64 bit, một chiều thì là 1, hai chiều chính là 1 << 22. Mảng二维 do tất cả heaparena组成 này cấu thành không gian bộ nhớ ảo của runtime go, nhìn tổng thể như hình dưới.

Mặc dù giữa các heaparena liền kề, nhưng trang bộ nhớ mà chúng quản lý giữa không liên tục.

mcache

mcache tương ứng với cấu trúc runtime.mcache, trong bài viết về concurrent scheduling đã từng xuất hiện, mặc dù tên của nó gọi là mcache nhưng thực tế nó được bind với processor P. mcache là bộ nhớ cache bộ nhớ của mỗi processor P, trong đó chứa linked list array alloc của mspan, kích thước array cố định là 136, vừa đúng là hai lần số lượng spanClass, còn có bộ nhớ cache đối tượng nhỏ tiny, trong đó tiny trỏ đến địa chỉ bắt đầu của bộ nhớ đối tượng nhỏ, tinyoffset là offset của bộ nhớ空闲相对于 địa chỉ bắt đầu, tinyAllocs biểu thị đã phân phối bao nhiêu đối tượng nhỏ. Về bộ nhớ cache stack stackcached, có thể đến Phân phối bộ nhớ stack để tìm hiểu.

go
type mcache struct {
    _ sys.NotInHeap

    nextSample uintptr // trigger heap sample after allocating this many bytes
    scanAlloc  uintptr // bytes of scannable heap allocated
    tiny       uintptr
    tinyoffset uintptr
    tinyAllocs uintptr

    alloc [numSpanClasses]*mspan
    stackcache [_NumStackOrders]stackfreelist
    flushGen atomic.Uint32
}

Khi vừa khởi tạo, linked list trong alloc của mcache đều chỉ chứa một node đầu rỗng runtime.emptymspan,也就是 không có mspan khả dụng.

go
func allocmcache() *mcache {
  var c *mcache
  systemstack(func() {
    lock(&mheap_.lock)
    c = (*mcache)(mheap_.cachealloc.alloc())
    c.flushGen.Store(mheap_.sweepgen)
    unlock(&mheap_.lock)
  })
  for i := range c.alloc {
    c.alloc[i] = &emptymspan
  }
  c.nextSample = nextSample()
  return c
}

Chỉ khi cần phân phối bộ nhớ, mới sẽ xin cấp từ mcentral một mspan mới để thay thế span rỗng ban đầu, phần công việc này do phương thức mcache.refill hoàn thành,入口 gọi duy nhất của nó là hàm runtime.mallocgc, dưới đây là code đã đơn giản hóa.

go
func (c *mcache) refill(spc spanClass) {
  // Return the current cached span to the central lists.
  s := c.alloc[spc]

  // Get a new cached span from the central lists.
  s = mheap_.central[spc].mcentral.cacheSpan()
  if s == nil {
    throw("out of memory")
  }

  c.scanAlloc = 0

  c.alloc[spc] = s
}

Lợi ích của việc sử dụng mcache là khi phân phối bộ nhớ không cần khóa toàn cục, nhưng khi bộ nhớ của nó không đủ cần truy cập mcentral, lúc này vẫn cần加锁。

mcentral

runtime.mcentral quản lý tất cả mspan lưu trữ đối tượng nhỏ trong heap, khi mcache xin cấp bộ nhớ cũng do mcentral进行 phân phối.

go
type mcentral struct {
    _         sys.NotInHeap
    spanclass spanClass
    partial [2]spanSet
    full    [2]spanSet
}

Trường của mcentral rất ít, spanClass biểu thị loại mspan được lưu trữ, partialfull là hai spanSet, cái trước lưu trữ mspan có bộ nhớ空闲, cái sau lưu trữ mspan không có bộ nhớ空闲。mcentral do heap mheap trực tiếp quản lý, trong runtime总共 có 136 mcentral.

go
type mheap struct {
    central [numSpanClasses]struct {
        mcentral mcentral
        pad      [(cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize) % cpu.CacheLinePadSize]byte
    }
}

mcentral chủ yếu负责 hai công việc, khi bộ nhớ đủ thì phân phối mspan khả dụng cho mcache, khi bộ nhớ không đủ thì xin cấp phân phối từ mheap một mspan mới. Công việc phân phối mspan cho mcache do phương thức mcentral.cacheSpan hoàn thành. Trước tiên sẽ tìm kiếm mspan khả dụng trong tập hợp đã quét của danh sách空闲。

go
// Try partial swept spans first.
sg := mheap_.sweepgen
if s = c.partialSwept(sg).pop(); s != nil {
    goto havespan
}

Nếu không tìm thấy, thì tìm kiếm mspan khả dụng trong tập hợp chưa quét của danh sách空闲

go
for ; spanBudget >= 0; spanBudget-- {
    s = c.partialUnswept(sg).pop()
    if s == nil {
        break
    }
    if s, ok := sl.tryAcquire(s); ok {
        s.sweep(true)
        sweep.active.end(sl)
        goto havespan
    }
}

Nếu vẫn không tìm thấy, thì đi tìm kiếm trong tập hợp chưa quét của danh sách không空闲

go
for ; spanBudget >= 0; spanBudget-- {
    s = c.fullUnswept(sg).pop()
    if s == nil {
        break
    }
    if s, ok := sl.tryAcquire(s); ok {
        s.sweep(true)
        freeIndex := s.nextFreeIndex()
        if freeIndex != s.nelems {
            s.freeindex = freeIndex
            sweep.active.end(sl)
            goto havespan
        }
        c.fullSwept(sg).push(s.mspan)
    }
}

Nếu cuối cùng vẫn không tìm thấy, thì sẽ do phương thức mcentral.grow xin cấp phân phối từ mheap một mspan mới.

go
s = c.grow()
if s == nil {
    return nil
}

Trong tình huống bình thường, bất kể thế nào đều sẽ trả về một mspan khả dụng.

go
havespan:
  freeByteBase := s.freeindex &^ (64 - 1)
  whichByte := freeByteBase / 8
  // Init alloc bits cache.
  s.refillAllocCache(whichByte)
  s.allocCache >>= s.freeindex % 64

  return s

Đối với quá trình xin cấp mspan từ mheap, thực tế là gọi phương thức mheap.alloc, phương thức này sẽ trả về một mspan mới.

go
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)
  if s == nil {
    return nil
  }

  n := s.divideByElemSize(npages << _PageShift)
  s.limit = s.base() + size*n
  s.initHeapBits(false)
  return s
}

Sau khi khởi tạo xong nó có thể phân phối cho mcache sử dụng.

mheap

runtimme.mheap là người quản lý bộ nhớ heap của ngôn ngữ go, trong runtime nó tồn tại như biến toàn cục runtime.mheap_.

go
var mheap_ mheap

Nó quản lý tất cả mspan được tạo, tất cả mcentral, và tất cả heaparena, còn có rất nhiều bộ phân phối khác, cấu trúc đã đơn giản hóa của nó như sau

go
type mheap struct {
    _ sys.NotInHeap

    lock mutex

    allspans []*mspan // all spans out there

    pagesInUse         atomic.Uintptr // pages of spans in stats mSpanInUse
    pagesSwept         atomic.Uint64  // pages swept this cycle
    pagesSweptBasis    atomic.Uint64  // pagesSwept to use as the origin of the sweep ratio

    arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
    allArenas []arenaIdx
    sweepArenas []arenaIdx
    central [numSpanClasses]struct {
        mcentral mcentral
        pad      [(cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize) % cpu.CacheLinePadSize]byte
    }

    pages            pageAlloc // page allocation data structure
    spanalloc              fixalloc // allocator for span*
    cachealloc             fixalloc // allocator for mcache*
    specialfinalizeralloc  fixalloc // allocator for specialfinalizer*
    specialprofilealloc    fixalloc // allocator for specialprofile*
    specialReachableAlloc  fixalloc // allocator for specialReachable
    specialPinCounterAlloc fixalloc // allocator for specialPinCounter
    arenaHintAlloc         fixalloc // allocator for arenaHints
}

Đối với mheap mà nói, trong runtime chủ yếu có bốn công việc cần làm

  • Khởi tạo heap
  • Phân phối mspan
  • Giải phóng mspan
  • Mở rộng heap

Dưới đây theo thứ tự来讲 bốn việc này.

Khởi tạo

Thời kỳ khởi tạo heap nằm ở giai đoạn bootstrap của chương trình, đồng thời cũng là giai đoạn khởi tạo của bộ điều phối, thứ tự gọi là

go
schedinit() -> mallocinit() -> mheap_.init()

Trong thời kỳ khởi tạo, nó chủ yếu负责 thực hiện công việc khởi tạo của các bộ phân phối

go
func (h *mheap) init() {
  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.specialfinalizeralloc.init(unsafe.Sizeof(specialfinalizer{}), nil, nil, &memstats.other_sys)
  h.specialprofilealloc.init(unsafe.Sizeof(specialprofile{}), nil, nil, &memstats.other_sys)
  h.specialReachableAlloc.init(unsafe.Sizeof(specialReachable{}), nil, nil, &memstats.other_sys)
  h.specialPinCounterAlloc.init(unsafe.Sizeof(specialPinCounter{}), nil, nil, &memstats.other_sys)
  h.arenaHintAlloc.init(unsafe.Sizeof(arenaHint{}), nil, nil, &memstats.other_sys)

  h.spanalloc.zero = false
  for i := range h.central {
    h.central[i].mcentral.init(spanClass(i))
  }

  h.pages.init(&h.lock, &memstats.gcMiscSys, false)
}

Trong đó bao gồm bộ phân phối负责 phân phối mspan mheap.spanalloc và bộ phân phối负责 phân phối trang mheap.pages, cũng như khởi tạo của tất cả mcentral.

Phân phối

Trong mheap, phân phối mspan đều do phương thức mheap.allocSpan hoàn thành

go
func (h *mheap) allocSpan(npages uintptr, typ spanAllocType, spanclass spanClass) (s *mspan)

Nếu bộ nhớ xin cấp phân phối đủ nhỏ, tức thỏa mãn npages < pageCachePages/4, thì sẽ thử không加锁 trong bộ nhớ cache mspan của P本地 để lấy một mspan khả dụng, nếu bộ nhớ cache của P là rỗng thì còn sẽ khởi tạo trước

go
// If the cache is empty, refill it.
if c.empty() {
    lock(&h.lock)
    *c = h.pages.allocToCache()
    unlock(&h.lock)
}

Rồi mới lấy từ bộ nhớ cache của P, do phương thức mheap.tryAllocMSpan hoàn thành.

go
pp := gp.m.p.ptr()
if !needPhysPageAlign && pp != nil && npages < pageCachePages/4 {
    c := &pp.pcache
    base, scav = c.alloc(npages)
    if base != 0 {
        s = h.tryAllocMSpan()
        if s != nil {
            goto HaveSpan
        }
    }
}

Code lấy mspan từ bộ nhớ cache của P như sau, nó sẽ thử lấy mspan cuối cùng trong bộ nhớ cache.

go
func (h *mheap) tryAllocMSpan() *mspan {
  pp := getg().m.p.ptr()
  // If we don't have a p or the cache is empty, we can't do
  // anything here.
  if pp == nil || pp.mspancache.len == 0 {
    return nil
  }
  // Pull off the last entry in the cache.
  s := pp.mspancache.buf[pp.mspancache.len-1]
  pp.mspancache.len--
  return s
}

Nếu bộ nhớ xin cấp phân phối khá lớn thì sẽ phân phối trên heap, quá trình này cần giữ khóa

go
lock(&h.lock)
if base == 0 {
    // Try to acquire a base address.
    base, scav = h.pages.alloc(npages)
    if base == 0 {
        var ok bool
        growth, ok = h.grow(npages)
        if !ok {
            unlock(&h.lock)
            return nil
        }
        base, scav = h.pages.alloc(npages)
        if base == 0 {
            throw("grew heap, but no adequate free space found")
        }
    }
}
if s == nil {
    // We failed to get an mspan earlier, so grab
    // one now that we have the heap lock.
    s = h.allocMSpanLocked()
}
unlock(&h.lock)

Trước tiên sẽ sử dụng pageAlloc.alloc để phân phối đủ trang bộ nhớ cho nó, nếu bộ nhớ heap không đủ thì sẽ do mheap.grow进行 mở rộng. Sau khi phân phối trang bộ nhớ xong, sẽ do bộ phân phối chuỗi mheap.spanalloc phân phối 64 mspan vào bộ nhớ cache của P本地, 64 vừa đúng là một nửa độ dài array bộ nhớ cache, rồi mới trả về một mspan khả dụng từ bộ nhớ cache của P.

go
func (h *mheap) allocMSpanLocked() *mspan {
  assertLockHeld(&h.lock)

  pp := getg().m.p.ptr()
  if pp == nil {
    // We don't have a p so just do the normal thing.
    return (*mspan)(h.spanalloc.alloc())
  }
  // Refill the cache if necessary.
  if pp.mspancache.len == 0 {
    const refillCount = len(pp.mspancache.buf) / 2
    for i := 0; i < refillCount; i++ {
      pp.mspancache.buf[i] = (*mspan)(h.spanalloc.alloc())
    }
    pp.mspancache.len = refillCount
  }
  // Pull off the last entry in the cache.
  s := pp.mspancache.buf[pp.mspancache.len-1]
  pp.mspancache.len--
  return s
}

Căn cứ vào hai tình huống trên, cuối cùng đều có thể nhận được một mspan khả dụng, cuối cùng sau khi khởi tạo mspan xong thì có thể trả về

go
HaveSpan:
  h.initSpan(s, typ, spanclass, base, npages)
  return s

Giải phóng

既然 mspan là do bộ phân phối chuỗi phân phối, tự nhiên khi giải phóng bộ nhớ cũng do nó进行 giải phóng.

go
func (h *mheap) freeSpanLocked(s *mspan, typ spanAllocType) {
  assertLockHeld(&h.lock)
  // Mark the space as free.
  h.pages.free(s.base(), s.npages)
  s.state.set(mSpanDead)
  h.freeMSpanLocked(s)
}

Trước tiên sẽ thông qua bộ phân phối trang mheap.pages đánh dấu chỉ định trang bộ nhớ được giải phóng, rồi đặt trạng thái của mspan thành mSpanDead, cuối cùng do bộ phân phối mheap.spanalloc giải phóng mspan.

go
func (h *mheap) freeMSpanLocked(s *mspan) {
  assertLockHeld(&h.lock)

  pp := getg().m.p.ptr()
  // First try to free the mspan directly to the cache.
  if pp != nil && pp.mspancache.len < len(pp.mspancache.buf) {
    pp.mspancache.buf[pp.mspancache.len] = s
    pp.mspancache.len++
    return
  }
  // Failing that (or if we don't have a p), just free it to
  // the heap.
  h.spanalloc.free(unsafe.Pointer(s))
}

Nếu bộ nhớ cache của P chưa đầy, thì sẽ đặt nó vào bộ nhớ cache của P本地 để tiếp tục sử dụng, nếu không thì nó sẽ được giải phóng trở lại bộ nhớ heap.

Mở rộng

Không gian bộ nhớ trang mà heaparena quản lý không phải đã được xin cấp hết trong thời kỳ đầu, chỉ khi cần dùng đến bộ nhớ mới đi phân phối. Cái负责 mở rộng bộ nhớ heap là phương thức mheap.grow, dưới đây là code đã đơn giản hóa.

go
func (h *mheap) grow(npage uintptr) (uintptr, bool) {
  assertLockHeld(&h.lock)
  ask := alignUp(npage, pallocChunkPages) * pageSize
  totalGrowth := uintptr(0)
  end := h.curArena.base + ask
  nBase := alignUp(end, physPageSize)

  if nBase > h.curArena.end || end < h.curArena.base {
    av, asize := h.sysAlloc(ask, &h.arenaHints, true)
        if uintptr(av) == h.curArena.end {
      h.curArena.end = uintptr(av) + asize
    } else {
      // Switch to the new space.
      h.curArena.base = uintptr(av)
      h.curArena.end = uintptr(av) + asize
    }
    nBase = alignUp(h.curArena.base+ask, physPageSize)
  }
  ...
}

Nó trước tiên sẽ tính toán dựa vào npage bộ nhớ cần thiết và căn chỉnh, rồi判断 xem heaparena hiện tại có đủ bộ nhớ không, nếu không đủ thì sẽ do mheap.sysAlloc cho heaparena hiện tại xin cấp thêm bộ nhớ hoặc phân phối một heaparena mới.

go
func (h *mheap) sysAlloc(n uintptr, hintList **arenaHint, register bool) (v unsafe.Pointer, size uintptr) {
  n = alignUp(n, heapArenaBytes)
  if hintList == &h.arenaHints {
    v = h.arena.alloc(n, heapArenaBytes, &gcController.heapReleased)
    if v != nil {
      size = n
      goto mapped
    }
  }
    ...
}

Trước tiên sẽ thử sử dụng bộ phân phối tuyến tính mheap.arena xin cấp một khối bộ nhớ trong không gian bộ nhớ đã phân phối trước, nếu thất bại thì dựa vào hintList进行 mở rộng, kiểu của hintListruntime.arenaHint, nó chuyên ghi lại thông tin địa chỉ liên quan đến mở rộng heaparena.

go
for *hintList != nil {
    hint := *hintList
    p := hint.addr
  v = sysReserve(unsafe.Pointer(p), n)
    if p == uintptr(v) {
        hint.addr = p
        size = n
        break
    }
    if v != nil {
        sysFreeOS(v, n)
    }
    *hintList = hint.next
    h.arenaHintAlloc.free(unsafe.Pointer(hint))
}

Sau khi xin cấp bộ nhớ xong, lại cập nhật nó vào array二维 arenas

go
for ri := arenaIndex(uintptr(v)); ri <= arenaIndex(uintptr(v)+size-1); ri++ {
    l2 := h.arenas[ri.l1()]
    var r *heapArena
    r = (*heapArena)(h.heapArenaAlloc.alloc(unsafe.Sizeof(*r), goarch.PtrSize, &memstats.gcMiscSys))
    atomic.StorepNoWB(unsafe.Pointer(&l2[ri.l2()]), unsafe.Pointer(r))
}

Cuối cùng lại do bộ phân phối trang đánh dấu这片 bộ nhớ là trạng thái sẵn sàng.

go
// Update the page allocator's structures to make this
// space ready for allocation.
h.pages.grow(v, nBase-v)
totalGrowth += nBase - v

Phân phối đối tượng

Khi go phân phối bộ nhớ cho đối tượng, dựa vào kích thước chia thành ba loại khác nhau:

  • Đối tượng nhỏ - tiny, nhỏ hơn 16B
  • Đối tượng nhỏ - small, nhỏ hơn 32KB
  • Đối tượng lớn - large, lớn hơn 32KB

Dựa vào ba loại khác nhau, khi phân phối bộ nhớ sẽ thực hiện logic khác nhau. Hàm负责 phân phối bộ nhớ cho đối tượng là runtime.mallocgc, signature của hàm như sau

go
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer

Nó chỉ có ba tham số, kích thước bộ nhớ, kiểu, và một boolean dùng để biểu thị có cần xóa bộ nhớ không. Nó là入口 hàm phân phối bộ nhớ của tất cả đối tượng go, bình thường khi sử dụng hàm new tạo pointer cũng sẽ đi vào hàm này, khi phân phối bộ nhớ thành công, pointer mà nó trả về chính là địa chỉ của đối tượng đó. Trong phần mspan đã đề cập, mỗi mspan đều sở hữu một spanClass, spanClass quyết định kích thước cố định của mspan, và go chia đối tượng từ [0, 32KB] phạm vi thành 68 loại kích thước khác nhau, nên bộ nhớ go do若干 mspan链表 cố định kích thước khác nhau组成。Khi phân phối bộ nhớ đối tượng, chỉ cần dựa vào kích thước đối tượng tính toán ra spanClass tương ứng, rồi dựa vào spanClass tìm mspan链表 tương ứng, cuối cùng lại từ链表中寻找 mspan khả dụng, cách làm phân cấp này có thể giải quyết vấn đề memory fragmentation khá hiệu quả.

Đối tượng nhỏ

Tất cả đối tượng nhỏ không phải pointer nhỏ hơn 16B sẽ do bộ phân phối nhỏ trong P được phân phối vào cùng一片 bộ nhớ liên tục, trong runitme.mcache, do trường tiny ghi lại địa chỉ cơ sở của这片 bộ nhớ.

go
type mcache struct {
  tiny       uintptr
  tinyoffset uintptr
  tinyAllocs uintptr
}

Kích thước của đối tượng nhỏ do hằng số runtime.maxTinySize quyết định, đều là 16B, khối bộ nhớ dùng để lưu trữ đối tượng nhỏ cũng là kích thước này, thông thường ở đây lưu trữ đối tượng đều là một số string nhỏ, phần code负责 phân phối đối tượng nhỏ như sau.

go
if size <= maxSmallSize {
    if noscan && size < maxTinySize {
      off := c.tinyoffset
      if off+size <= maxTinySize && c.tiny != 0 {
        x = unsafe.Pointer(c.tiny + off)
        c.tinyoffset = off + size
        c.tinyAllocs++
        mp.mallocing = 0
        releasem(mp)
        return x
      }

      // Allocate a new maxTinySize block.
      span = c.alloc[tinySpanClass]
      v := nextFreeFast(span)
      if v == 0 {
        v, span, shouldhelpgc = c.nextFree(tinySpanClass)
      }
      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

Nếu khối bộ nhớ nhỏ hiện tại vẫn có đủ không gian để chứa, thì trực tiếp sử dụng khối bộ nhớ hiện tại, tức off+size <= maxTinySize. Nếu không đủ thì sẽ trước tiên thử từ bộ nhớ cache span của mcache tìm kiếm không gian khả dụng, nếu cũng không được thì sẽ xin cấp từ mcentral một mspan, bất kể thế nào cuối cùng đều sẽ nhận được một địa chỉ khả dụng, cuối cùng lại dùng khối bộ nhớ đối tượng nhỏ mới thay thế khối cũ.

Đối tượng nhỏ

Phần lớn đối tượng trong ngôn ngữ go runtime đều là đối tượng nhỏ nằm trong phạm vi [16B, 32KB] này, quá trình phân phối đối tượng nhỏ phiền phức nhất, nhưng code lại là ít nhất, phần code负责 phân phối đối tượng nhỏ như sau.

go
var sizeclass uint8
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)
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(x, size)
}

Trước tiên sẽ dựa vào kích thước đối tượng tính toán xem nên sử dụng spanClass loại nào, rồi do runtime.nextFreeFast dựa vào spanClass thử đi vào bộ nhớ cache mspan tương ứng trong mcache lấy không gian bộ nhớ khả dụng.

go
func nextFreeFast(s *mspan) gclinkptr {
  theBit := sys.TrailingZeros64(s.allocCache) // Is there a free object in the 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 >>= uint(theBit + 1)
      s.freeindex = freeidx
      s.allocCount++
      return gclinkptr(result*s.elemsize + s.base())
    }
  }
  return 0
}

Tác dụng của mspan.allocCache là ghi lại xem không gian bộ nhớ có đối tượng sử dụng không, và nó dựa vào số lượng đối tượng để划分 bộ nhớ từng cái một chứ không phải dựa vào kích thước không gian để划分, điều này tương đương với xem mspan như một array đối tượng, như hình dưới.

allocCache là một số 64 bit, mỗi bit tương ứng với一片 không gian bộ nhớ, nếu bit nào đó là 0 biểu thị có đối tượng sử dụng, nếu là 1 thì biểu thị这片 bộ nhớ là空闲。Mục đích của sys.TrailingZeros64(s.allocCache) là tính số lượng zero đuôi, nếu kết quả là 64 thì biểu thị không có bộ nhớ空闲 có thể sử dụng, nếu có thì lại tính toán得到 offset của bộ nhớ空闲 cộng với địa chỉ cơ sở của mspan rồi trả về.

Khi mcache không có đủ không gian, thì sẽ đi xin cấp từ mcentral, phần công việc này do phương thức mcache.nextFree hoàn thành

go
func (c *mcache) nextFree(spc spanClass) (v gclinkptr, s *mspan, shouldhelpgc bool) {
  s = c.alloc[spc]
  shouldhelpgc = false
  freeIndex := s.nextFreeIndex()
  if freeIndex == s.nelems {
    c.refill(spc)
    shouldhelpgc = true
    s = c.alloc[spc]

    freeIndex = s.nextFreeIndex()
  }
  v = gclinkptr(freeIndex*s.elemsize + s.base())
  s.allocCount++
  return
}

Trong đó mcache.refill sẽ负责 xin cấp từ mcentral một mspan khả dụng.

go
func (c *mcache) refill(spc spanClass) {
  ...
  s = mheap_.central[spc].mcentral.cacheSpan()
  ...
}

Mà phương thức mcentral.cacheSpan sẽ do mcentral.grow进行 mở rộng khi bộ nhớ không đủ, mở rộng thì lại sẽ đi xin cấp từ mheap mspan mới.

go
func (c *mcentral) grow() *mspan {
  ...
  s := mheap_.alloc(npages, c.spanclass)
  ...
  return s
}

Nên cuối cùng xem ra, phân phối bộ nhớ đối tượng nhỏ là từng cấp từng cấp đi xuống, trước là mcache, rồi là mcentral, cuối cùng là mheap. Chi phí phân phối của mcache thấp nhất, vì nó là bộ nhớ cache của P本地, phân phối bộ nhớ không cần giữ khóa, mcentral其次, trực tiếp xin cấp bộ nhớ từ mheap chi phí cao nhất, vì phương thức mheap.alloc sẽ cạnh tranh khóa toàn cục của cả heap.

Đối tượng lớn

Phân phối đối tượng lớn đơn giản nhất, nếu kích thước đối tượng vượt quá 32KB, thì sẽ trực tiếp xin cấp từ mheap phân phối một mspan mới để chứa, phần code负责 phân phối đối tượng lớn như sau.

go
shouldhelpgc = true
span = c.allocLarge(size, noscan)
span.freeindex = 1
span.allocCount = 1
size = span.elemsize
x = unsafe.Pointer(span.base())
if needzero && span.needzero != 0 {
    if noscan {
        delayedZeroing = true
    } else {
        memclrNoHeapPointers(x, size)
    }
}

Trong đó mcache.allocLarge负责 xin cấp từ mheap không gian bộ nhớ của đối tượng lớn

go
func (c *mcache) allocLarge(size uintptr, noscan bool) *mspan {
  ...
  spc := makeSpanClass(0, noscan)
  s := mheap_.alloc(npages, spc)
  ...
  return s
}

Từ code có thể thấy là spanClass mà đối tượng lớn sử dụng giá trị là 0, đối tượng lớn về cơ bản là một đối tượng chiếm một mpan.

Khác

Thống kê bộ nhớ

Go runtime expose cho người dùng một hàm ReadMemStats, có thể dùng để thống kê tình hình bộ nhớ của runtime.

go
func ReadMemStats(m *MemStats) {
  _ = m.Alloc // nil check test before we switch stacks, see issue 61158
  stopTheWorld(stwReadMemStats)

  systemstack(func() {
    readmemstats_m(m)
  })

  startTheWorld()
}

Nhưng cái giá phải trả cho việc sử dụng nó rất lớn, từ code có thể thấy trước khi phân tích tình hình bộ nhớ cần STW, mà thời gian STW có thể là vài mili giây đến vài trăm mili giây không đều, thông thường chỉ sử dụng khi debug và troubleshooting. Cấu trúc runtime.MemStats ghi lại thông tin liên quan đến bộ nhớ heap, bộ nhớ stack, và GC

go
type MemStats struct {
    //  总体统计
    Alloc uint64
    TotalAlloc uint64
    Sys uint64
    Lookups uint64
    Mallocs uint64
    Frees uint64

    // 堆内存统计
    HeapAlloc uint64
    HeapSys uint64
    HeapIdle uint64
    HeapInuse uint64
    HeapReleased uint64
    HeapObjects uint64

    // 栈内存统计
    StackInuse uint64
    StackSys uint64

    // 内存组件统计
    MSpanInuse uint64
    MSpanSys uint64
    MCacheInuse uint64
    MCacheSys uint64
    BuckHashSys uint64

    // gc 相关的统计
    GCSys uint64
    OtherSys uint64
    NextGC uint64
    LastGC uint64
    PauseTotalNs uint64
    PauseNs [256]uint64
    PauseEnd [256]uint64
    NumGC uint32
    NumForcedGC uint32
    GCCPUFraction float64
    EnableGC bool
    DebugGC bool

    BySize [61]struct {
        Size uint32
        Mallocs uint64
        Frees uint64
    }
}

NotInHeap

Bộ phân phối bộ nhớ rõ ràng dùng để phân phối bộ nhớ heap, nhưng heap lại được chia thành hai phần, một phần là bộ nhớ heap mà go runtime tự thân cần, phần khác là bộ nhớ heap mở cho người dùng sử dụng. Nên trong một số cấu trúc có thể thấy trường embedded như vậy

go
_ sys.NotInHeap

Biểu thị bộ nhớ của kiểu này sẽ không được phân phối trên user heap, loại trường embedded này trong component phân phối bộ nhớ尤为 phổ biến, ví dụ như cấu trúc thể hiện user heap runtime.mheap

go
type mheap struct {
  _ sys.NotInHeap
}

Tác dụng thực sự của sys.NotInHeap là để tránh memory barrier nhằm nâng cao hiệu suất runtime, mà user heap cần chạy GC nên cần memory barrier.

Golang by www.golangdev.cn edit