Skip to content

memory

Berbeda dengan c/c++ tradisional, go adalah bahasa dengan gc, dalam kebanyakan kasus alokasi dan penghancuran memori dikelola secara otomatis oleh go, apakah memori objek harus dialokasikan di stack atau heap ditentukan oleh compiler, pada dasarnya tidak perlu pengguna berpartisipasi dalam manajemen memori, yang perlu dilakukan pengguna hanyalah menggunakan memori. Dalam go, manajemen memori heap terutama memiliki dua komponen besar, alokator memori bertanggung jawab untuk alokasi memori heap, garbage collector bertanggung jawab untuk回收 memori heap yang tidak berguna, artikel ini terutama membahas cara kerja alokator memori, alokator memori go sangat dipengaruhi oleh alokator memori TCMalloc Google.

Alokator

Dalam go terdapat dua jenis alokator memori, satu adalah alokator linear, yang lainnya adalah alokator linked list.

Alokasi Linear

Alokator linear sesuai dengan struktur runtime.linearAlloc, seperti yang ditunjukkan di bawah ini

go
type linearAlloc struct {
  next   uintptr // byte berikutnya yang tersedia
  mapped uintptr // satu byte setelah akhir ruang yang dipetakan
  end    uintptr // akhir ruang yang dicadangkan

  mapMemory bool // transisi memori dari Reserved ke Ready jika true
}

Alokator ini akan meminta ruang memori kontinu dari sistem operasi di awal, next menunjuk ke alamat memori yang dapat digunakan, end menunjuk ke alamat akhir ruang memori, kira-kira dapat dipahami seperti gambar berikut.

Cara alokasi memori alokator linear sangat mudah dipahami, berdasarkan ukuran memori yang akan diminta periksa apakah ada ruang sisa yang cukup untuk menampung, jika cukup maka perbarui field next dan return alamat awal ruang sisa, kodenya seperti berikut.

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)
}

Keuntungan dari cara alokasi ini adalah cepat dan sederhana, kekurangannya juga cukup jelas, yaitu tidak dapat memanfaatkan kembali memori yang telah dilepaskan, karena field next hanya akan menunjuk ke alamat memori ruang sisa, untuk ruang memori yang telah digunakan kemudian dilepaskan tidak dapat dirasakan, hal ini akan menyebabkan banyak pemborosan ruang memori, seperti yang ditunjukkan pada gambar berikut.

Jadi alokasi linear bukan cara alokasi utama dalam go, ia hanya digunakan sebagai fungsi pre-alokasi memori pada mesin 32-bit.

Alokasi Linked List

Alokator linked list sesuai dengan struktur runtime.fixalloc, memori yang dialokasikan oleh alokator linked list tidak kontinu, ada dalam bentuk linked list satu arah. Alokator linked list terdiri dari beberapa blok memori berukuran tetap, dan setiap blok memori terdiri dari beberapa slice memori berukuran tetap, setiap kali melakukan alokasi memori, akan menggunakan satu slice memori berukuran tetap.

go
type fixalloc struct {
  size   uintptr
  first  func(arg, p unsafe.Pointer) // dipanggil pertama kali p dikembalikan
  arg    unsafe.Pointer
  list   *mlink
  chunk  uintptr // gunakan uintptr alih-alih unsafe.Pointer untuk menghindari write barrier
  nchunk uint32  // byte yang tersisa di chunk saat ini
  nalloc uint32  // ukuran chunk baru dalam byte
  inuse  uintptr // byte yang sedang digunakan sekarang
  stat   *sysMemStat
  zero   bool // alokasi nol
}

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

Field-nya tidak sederhana dan mudah dipahami seperti alokator linear, di sini saya perkenalkan sebagian yang penting

  • size, mengacu pada berapa banyak memori yang digunakan setiap kali alokasi memori.
  • list, menunjuk ke node kepala slice memori yang dapat digunakan kembali, setiap slice memori berukuran ditentukan oleh size.
  • chunk, menunjuk ke alamat空闲 di blok memori yang sedang digunakan saat ini
  • nchunk, jumlah byte yang tersedia di blok memori saat ini
  • nalloc, ukuran blok memori, tetap 16KB.
  • inuse, total berapa byte memori yang telah digunakan
  • zero, apakah akan mengosongkan memori saat menggunakan kembali blok memori

Alokator linked list memegang referensi ke blok memori saat ini dan slice memori yang dapat digunakan kembali, ukuran setiap blok memori tetap 16KB, nilai ini sudah diatur saat inisialisasi.

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
}

Distribusi blok memori seperti yang ditunjukkan pada gambar berikut, blok memori dalam gambar diatur berdasarkan waktu pembuatan, sebenarnya alamat mereka tidak kontinu.

Ukuran memori yang dialokasikan setiap kali oleh alokator linked list juga tetap, ditentukan oleh fixalloc.size, saat alokasi akan memeriksa terlebih dahulu apakah ada blok memori yang dapat digunakan kembali, jika ada maka akan mengutamakan penggunaan blok memori yang dapat digunakan kembali, kemudian baru menggunakan blok memori saat ini, jika ruang sisa blok memori saat ini tidak cukup untuk menampung akan membuat blok memori baru, bagian logika ini sesuai dengan kode berikut.

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
}

Keuntungan alokator linked list adalah dapat menggunakan kembali memori yang dilepaskan, unit dasar untuk menggunakan kembali memori adalah slice memori berukuran tetap, ukurannya ditentukan oleh fixalloc.size, saat melepaskan memori, alokator linked list akan menambahkan slice memori ini sebagai node kepala ke linked list slice memori空闲, kodenya seperti yang ditunjukkan di bawah ini

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

Komponen Memori

Alokator memori dalam go terutama terdiri dari beberapa komponen mspan, heaparena, mcache, mcentral, mheap, mereka bekerja berlapis-lapis, mengelola seluruh memori heap go.

mspan

runtime.mspan adalah unit dasar dalam alokasi memori go, strukturnya seperti berikut

go
type mspan struct {
    next *mspan     // span berikutnya dalam list, atau nil jika tidak ada
    prev *mspan     // span sebelumnya dalam list, atau nil jika tidak ada

    startAddr uintptr // alamat byte pertama span alias s.base()
    npages    uintptr // jumlah halaman dalam span
    freeindex uintptr

    spanclass             spanClass     // class ukuran dan noscan (uint8)
    needzero              uint8         // perlu di-nol-kan sebelum alokasi
    elemsize              uintptr       // dihitung dari sizeclass atau dari npages
    limit                 uintptr       // akhir data dalam span
    state                 mSpanStateBox // mSpanInUse dll; diakses secara atomik (metode get/set)

    nelems uintptr // jumlah objek dalam span.
    allocCache uint64
    allocCount            uint16        // jumlah objek yang dialokasikan
    ...
}

mspan dan mspan dihubungkan dalam bentuk doubly linked list melalui next dan prev, alamat memori tidak kontinu. Setiap mspan mengelola memori halaman sebesar runtime.pageSize sebanyak mspan.npages, biasanya ukuran halaman adalah 8KB, dan dicatat oleh mspan.startAddr alamat awal halaman-halaman ini dan mspan.limit mencatat alamat akhir memori yang telah digunakan. Ukuran elemen yang disimpan setiap mspan elemsize adalah tetap, jadi jumlah elemen yang dapat ditampung juga tetap. Karena jumlahnya tetap, penyimpanan objek seperti array didistribusikan di mspan, rentangnya [0, nelems], sekaligus dicatat oleh freeindex indeks berikutnya yang tersedia untuk menyimpan objek. mspan total memiliki tiga status

  • mSpanDead, memori telah dilepaskan
  • mSpanInUse, dialokasikan ke heap
  • mSpanManual, dialokasikan ke bagian untuk manajemen memori manual, seperti stack.

Yang menentukan ukuran elemen mspan adalah spanClass, spanClass sendiri adalah integer tipe uint8, tujuh bit tinggi menyimpan nilai class 0-67, bit terakhir digunakan untuk menunjukkan noscan yaitu apakah berisi pointer.

go
type spanClass uint8

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

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

Ia total memiliki 68 nilai berbeda, semua nilai disimpan dalam bentuk tabel di file runtime.sizeclasses.go, saat runtime, menggunakan spanClass melalui runtime.class_to_size dapat memperoleh ukuran objek mspan, melalui class_to_allocnpages dapat memperoleh jumlah halaman mspan.

classukuran objek maksimumukuran spanjumlah objekpemborosan tailtingkat pemborosan memori maksimumalignment minimum
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

Tentang logika perhitungan nilai-nilai ini dapat ditemukan di fungsi printComment di runtime.mksizeclasses.go, di antaranya rumus perhitungan tingkat pemborosan memori maksimum adalah

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

Misalnya, ketika class adalah 2, tingkat pemborosan memori maksimumnya adalah

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

Ketika nilai class adalah 0, ini adalah spanClass yang khusus digunakan untuk mengalokasikan objek besar di atas 32KB, pada dasarnya satu objek besar akan menempati satu mspan. Jadi, heap go sebenarnya terdiri dari beberapa linked list mspan dengan ukuran tetap yang berbeda.

heaparena

Sebelumnya disebutkan bahwa mspan terdiri dari beberapa halaman, tetapi mspan hanya memegang referensi alamat halaman, tidak bertanggung jawab untuk mengelola halaman-halaman ini, yang benar-benar bertanggung jawab untuk mengelola memori halaman ini adalah runtime.heaparena. Setiap heaparena mengelola beberapa halaman, ukuran heaparena ditentukan oleh runtime.heapArenaBytes, biasanya 64MB. bitmap digunakan untuk mengidentifikasi apakah alamat yang sesuai dalam halaman存放 objek, zeroedBase adalah alamat awal memori halaman yang dikelola oleh heaparena ini, dan dicatat oleh spans halaman mana yang digunakan oleh mspan mana.

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
}

Tentang logika pencatatan halaman dan mspan dapat ditemukan di metode mheap.setSpans, seperti yang ditunjukkan di bawah ini

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
  }
}

Dalam heap go,是由一个二维的heaparena数组来管理所有的页内存,参见mheap.arenas字段。

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

Di platform windows 64-bit, array satu dimensi adalah 1 << 6, dua dimensi adalah 1 << 16, di platform linux 64-bit, satu dimensi adalah 1, dua dimensi adalah 1 << 22. Array dua dimensi yang terdiri dari semua heaparena ini membentuk ruang memori virtual runtime go, secara keseluruhan seperti yang ditunjukkan pada gambar berikut.

Meskipun heaparena berdekatan, memori halaman yang mereka kelola tidak kontinu.

mcache

mcache sesuai dengan struktur runtime.mcache, sudah muncul dalam artikel concurrent scheduling, meskipun namanya mcache tetapi sebenarnya terikat dengan processor P. mcache adalah cache memori di setiap processor P,其中包括mspan linked list数组alloc, ukuran array tetap136, tepat dua kali jumlah spanClass,还有微对象缓存tiny,其中tiny指向微对象内存的起始地址,tinyoffset则是空闲内存相对于起始地址的偏移量,tinyAllocs表示分配了多少个微对象。关于栈缓存stackcached,可以前往栈内存分配进行了解。

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
}

Saat inisialisasi, linked list di alloc di mcache hanya berisi node kepala kosong runtime.emptymspan, yaitu mspan tanpa memori yang tersedia.

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
}

Hanya ketika perlu melakukan alokasi memori, akan meminta mspan baru dari mcentral untuk mengganti span kosong asli, bagian pekerjaan ini diselesaikan oleh metode mcache.refill, satu-satunya入口 pemanggilan adalah fungsi runtime.mallocgc, berikut adalah kode yang disederhanakan.

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
}

Keuntungan menggunakan mcache adalah alokasi memori tidak memerlukan lock global, tetapi ketika memorinya tidak cukup perlu mengakses mcentral, saat ini masih perlu加锁。

mcentral

runtime.mcentral mengelola semua mspan yang存放 objek kecil di heap, saat mcache meminta memori juga dialokasikan oleh mcentral.

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

Field mcentral sangat sedikit, spanClass menunjukkan jenis mspan yang disimpan, partial dan full adalah dua spanSet, yang pertama存放mspan dengan memori空闲, yang kedua存放mspan tanpa memori空闲。mcentral dikelola langsung oleh heap mheap, saat runtime total ada 136 mcentral.

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

mcentral terutama bertanggung jawab untuk dua pekerjaan, ketika memori cukup mengalokasikan mspan yang tersedia ke mcache, ketika memori tidak cukup meminta alokasi mspan baru dari mheap. Pekerjaan mengalokasikan mspan ke mcache diselesaikan oleh metode mcentral.cacheSpan. Pertama akan mencari mspan yang tersedia di set yang telah dibersihkan di daftar空闲。

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

Jika tidak ditemukan, cari mspan yang tersedia di set yang belum dibersihkan di daftar空闲

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
    }
}

Jika masih tidak ditemukan, cari di set yang belum dibersihkan di daftar non-idle

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)
    }
}

Jika pada akhirnya masih tidak ditemukan, maka akan oleh metode mcentral.grow meminta alokasi mspan baru dari mheap.

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

Dalam situasi normal, bagaimanapun akan return mspan yang tersedia.

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

  return s

Untuk proses meminta mspan dari mheap, sebenarnya memanggil metode mheap.alloc, metode ini akan return mspan baru.

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
}

Setelah menginisialisasinya dengan baik dapat dialokasikan ke mcache untuk digunakan.

mheap

runtimme.mheap adalah pengelola memori heap bahasa go, saat runtime ada sebagai variabel global runtime.mheap_.

go
var mheap_ mheap

Ia mengelola semua mspan yang dibuat, semua mcentral, dan semua heaparena, serta banyak alokator lainnya, strukturnya yang disederhanakan seperti berikut

go
type mheap struct {
    _ sys.NotInHeap

    lock mutex

    allspans []*mspan // semua span di luar sana

    pagesInUse         atomic.Uintptr // halaman span dalam statistik mSpanInUse
    pagesSwept         atomic.Uint64  // halaman yang disapu siklus ini
    pagesSweptBasis    atomic.Uint64  // pagesSwept untuk digunakan sebagai asal rasio sweep

    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 // struktur data alokasi halaman
    spanalloc              fixalloc // alokator untuk span*
    cachealloc             fixalloc // alokator untuk mcache*
    specialfinalizeralloc  fixalloc // alokator untuk specialfinalizer*
    specialprofilealloc    fixalloc // alokator untuk specialprofile*
    specialReachableAlloc  fixalloc // alokator untuk specialReachable
    specialPinCounterAlloc fixalloc // alokator untuk specialPinCounter
    arenaHintAlloc         fixalloc // alokator untuk arenaHints
}

Untuk mheap, saat runtime terutama ada empat pekerjaan yang harus dilakukan

  • Inisialisasi heap
  • Alokasi mspan
  • Pelepasan mspan
  • Ekspansi heap

Di bawah ini akan menceritakan keempat hal ini sesuai urutan.

Inisialisasi

Periode inisialisasi heap terletak pada tahap bootstrap program, sekaligus tahap inisialisasi scheduler, urutan pemanggilan adalah

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

Pada periode inisialisasi, ia terutama bertanggung jawab untuk melaksanakan pekerjaan inisialisasi masing-masing alokator

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)
}

Di antaranya termasuk alokator mheap.spanalloc yang bertanggung jawab untuk mengalokasikan mspan dan alokator mheap.pages yang bertanggung jawab untuk alokasi halaman, serta inisialisasi semua mcentral.

Alokasi

Di mheap, alokasi mspan semuanya diselesaikan oleh metode mheap.allocSpan

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

Jika memori yang diminta untuk dialokasikan cukup kecil, yaitu memenuhi npages < pageCachePages/4, maka akan mencoba不加锁 di cache mspan di P lokal untuk mendapatkan mspan yang tersedia, jika cache P kosong, akan menginisialisasi terlebih dahulu

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

Kemudian mendapatkan dari cache P, diselesaikan oleh metode mheap.tryAllocMSpan.

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
        }
    }
}

Kode untuk mendapatkan mspan dari cache P seperti berikut, ia akan mencoba mendapatkan mspan terakhir di 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
}

Jika memori yang diminta cukup besar, akan mengalokasikan memori di heap, proses ini perlu持有锁

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)

Pertama akan menggunakan pageAlloc.alloc untuk mengalokasikan memori halaman yang cukup, jika memori heap tidak cukup akan oleh mheap.grow untuk ekspansi. Setelah alokasi memori halaman selesai, akan oleh alokator linked list mheap.spanalloc mengalokasikan 64 mspan ke cache lokal P, 64 tepat setengah dari panjang array cache, kemudian return mspan yang tersedia dari cache 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
}

Menurut dua situasi di atas, pada akhirnya dapat mendapatkan mspan yang tersedia, terakhir setelah menginisialisasi mspan dapat return

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

Pelepasan

Karena mspan dialokasikan oleh alokator linked list, secara alami saat melepaskan memori juga olehnya untuk melepaskan.

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)
}

Pertama akan menandai melalui alokator halaman mheap.pages bahwa memori halaman yang ditentukan telah dilepaskan, kemudian mengatur status mspan menjadi mSpanDead, terakhir oleh alokator mheap.spanalloc melepaskan 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))
}

Jika cache P belum penuh, akan menempatkannya di cache lokal P untuk terus digunakan, jika tidak akan dilepaskan kembali ke memori heap.

Ekspansi

Ruang memori halaman yang dikelola oleh heaparena tidak semuanya sudah diminta di awal, hanya akan dialokasikan ketika perlu menggunakan memori. Yang bertanggung jawab untuk ekspansi memori heap adalah metode mheap.grow, berikut adalah kode yang disederhanakan.

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)
  }
  ...
}

Ia pertama akan menghitung memori yang diperlukan berdasarkan npage dan melakukan alignment, kemudian判断 apakah heaparena saat ini memiliki memori yang cukup, jika tidak cukup akan oleh mheap.sysAlloc meminta lebih banyak memori untuk heaparena saat ini atau mengalokasikan heaparena baru.

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
    }
  }
    ...
}

Pertama akan mencoba menggunakan alokator linear mheap.arena untuk meminta sepotong memori di ruang memori yang telah dialokasikan di awal, jika gagal akan melakukan ekspansi berdasarkan hintList, tipe hintList adalah runtime.arenaHint, ia khusus mencatat informasi alamat terkait ekspansi 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))
}

Setelah memori diminta, kemudian perbarui ke array dua dimensi 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))
}

Terakhir再由页分配器将这片内存标记为就绪状态。

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

Alokasi Objek

Go saat mengalokasikan memori untuk objek, berdasarkan ukuran dibagi menjadi tiga jenis yang berbeda:

  • Objek mikro - tiny, kurang dari 16B
  • Objek kecil - small, kurang dari 32KB
  • Objek besar - large, lebih dari 32KB

Berdasarkan tiga jenis yang berbeda, saat alokasi memori akan menjalankan logika yang berbeda. Fungsi yang bertanggung jawab untuk mengalokasikan memori objek adalah runtime.mallocgc, signature fungsinya seperti berikut

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

Ia hanya memiliki tiga parameter, ukuran memori, tipe, dan nilai boolean用于表示是否需要清空内存。它是所有 go 对象内存分配的入口函数,平时在使用new函数创建指针时同样也会走入该函数,当内存分配成功后,它返回的指针就是该对象的地址。在 mspan 部分中提到过,每一个 mspan 都拥有一个spanClass,spanClass决定了mspan 的固定大小,并且 go 将对象从 [0, 32KB] 的范围分成了 68 种不同的大小,所以 go 内存由若干个不同的大小固定的mspan 链表组成。在分配对象内存时,只需按照对象大小计算出对应的spanClass,然后再根据spanClass 找到对应的mspan 链表,最后再从链表中寻找可用的mspan,这种分级的做法能较为有效的解决内存碎片的问题。

Objek Mikro

Semua objek mikro non-pointer yang kurang dari 16B akan oleh alokator mikro di P dialokasikan ke sepotong memori kontinu yang sama, di runitme.mcache, field tiny mencatat alamat dasar memori ini.

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

Ukuran objek mikro ditentukan oleh konstanta runtime.maxTinySize, semuanya 16B,用于存储微对象的内存块同样也是这个大小,一般来说这里存储的对象都是一些小字符串,负责分配微对象的部分代码如下所示。

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

Jika blok memori mikro saat ini masih memiliki ruang yang cukup untuk menampung, langsung gunakan blok memori saat ini, yaitu off+size <= maxTinySize. Jika tidak cukup, akan pertama mencoba mencari ruang yang tersedia dari cache span mcache, jika juga tidak bisa akan meminta mspan dari mcentral, bagaimanapun pada akhirnya akan mendapatkan alamat yang tersedia, terakhir再用新的微对象内存块替换掉旧的。

Objek Kecil

Sebagian besar objek dalam runtime bahasa go adalah objek kecil dalam rentang [16B, 32KB], proses alokasi objek kecil paling merepotkan, tetapi kodenya paling sedikit, bagian kode yang bertanggung jawab untuk alokasi objek kecil seperti berikut.

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)
}

Pertama akan menghitung berdasarkan ukuran objek应该使用哪一类的spanClass, kemudian oleh runtime.nextFreeFast berdasarkan spanClass mencoba mendapatkan ruang memori yang tersedia dari cache mspan yang sesuai di mcache.

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
}

Fungsi mspan.allocCache adalah untuk mencatat apakah ruang memori telah digunakan oleh objek, dan ia membagi memori satu per satu berdasarkan jumlah objek bukan berdasarkan ukuran ruang, ini相当于把mspan 看作一个对象数组,如下图所示。

allocCache adalah angka 64-bit, setiap bit sesuai dengan sepotong ruang memori, jika某一位为 0 表示有对象使用,如果是 1 的话表示这片内存是空闲的。sys.TrailingZeros64(s.allocCache) 的目的就是计算尾随零的数量,如果结果是 64 的话则表明没有空闲的内存可以使用,如果有的话再计算得到空闲内存的偏移量加上mspan 的基地址然后返回。

Ketika mcache tidak memiliki ruang yang cukup, akan pergi ke mcentral untuk meminta, bagian pekerjaan ini diselesaikan oleh metode mcache.nextFree

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
}

Di antaranya mcache.refill akan bertanggung jawab untuk meminta mspan yang tersedia dari mcentral.

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

Dan metode mcentral.cacheSpan akan melakukan ekspansi oleh mcentral.grow ketika memori tidak cukup, ekspansi kemudian akan meminta mspan baru dari mheap.

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

Jadi pada akhirnya terlihat, alokasi memori objek kecil adalah berjalan tingkat demi tingkat ke bawah, pertama mcache, kemudian mcentral, terakhir mheap. Biaya alokasi mcache paling rendah, karena ia adalah cache lokal P, alokasi memori tidak perlu持有锁,mcentral berikutnya, langsung meminta memori dari mheap biaya paling tinggi, karena metode mheap.alloc akan bersaing lock global seluruh heap.

Objek Besar

Alokasi objek besar paling sederhana, jika ukuran objek melebihi 32KB, akan langsung meminta alokasi mspan baru dari mheap untuk menampung, bagian kode yang bertanggung jawab untuk alokasi objek besar seperti berikut.

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)
    }
}

Di antaranya mcache.allocLarge bertanggung jawab untuk meminta ruang memori objek besar dari mheap

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

Dari kode dapat dilihat bahwa spanClass yang digunakan objek besar adalah nilai 0, pada dasarnya satu objek besar menempati satu mspan.

Lainnya

Statistik Memori

Runtime go mengekspos fungsi ReadMemStats kepada pengguna, dapat digunakan untuk统计 situasi memori 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()
}

Tetapi biaya penggunaannya sangat besar, dari kode dapat dilihat sebelum menganalisis situasi memori perlu STW, dan durasi STW mungkin beberapa milidetik hingga ratusan milidetik, umumnya hanya digunakan saat debugging dan troubleshooting. Struktur runtime.MemStats mencatat informasi terkait memori heap, memori stack, dan 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

Alokator memori jelas digunakan untuk mengalokasikan memori heap, tetapi heap dibagi menjadi dua bagian, satu bagian adalah memori heap yang dibutuhkan oleh runtime go sendiri, bagian lainnya adalah memori heap yang dibuka untuk pengguna. Jadi di beberapa struktur dapat melihat field embedded seperti ini

go
_ sys.NotInHeap

Menunjukkan bahwa memori tipe ini tidak akan dialokasikan di heap pengguna, field embedded ini terutama umum dalam komponen alokasi memori, seperti struktur yang表示用户堆的结构体runtime.mheap

go
type mheap struct {
  _ sys.NotInHeap
}

Fungsi sebenarnya dari sys.NotInHeap adalah untuk menghindari memory barrier guna meningkatkan efisiensi runtime, sedangkan heap pengguna perlu menjalankan GC sehingga memerlukan memory barrier.

Golang by www.golangdev.cn edit