Skip to content

memory

Geleneksel C/C++'dan farklı olarak, Go bir GC dilidir. Çoğu durumda, bellek tahsisi ve serbest bırakma Go tarafından otomatik olarak yönetilir. Derleyici bir nesnenin belleğinin yığında mı yoksa yığından mı tahsis edileceğine karar verir, temelde kullanıcı bellek yönetimine katılmadan. Kullanıcılar sadece belleği kullanmalıdır. Go'da, yığın bellek yönetimi principalmente iki büyük bileşenden oluşur: bellek tahsis edici yığın bellek tahsisinden sorumludur ve çöp toplayıcı kullanılmayan yığın belleğini geri dönüştürmek ve serbest bırakmaktan sorumludur. Bu makale principalmente bellek tahsis edicinin nasıl çalıştığını tartışır. Go'nun bellek tahsis edicisi büyük ölçüde Google'ın TCMalloc bellek tahsis edicisinden etkilenmiştir.

Tahsis Ediciler

Go'da, iki tür bellek tahsis edici vardır: biri doğrusal tahsis edici ve diğeri bağlı tahsis edici.

Doğrusal Tahsis

Doğrusal tahsis edici runtime.linearAlloc yapısına karşılık gelir, aşağıda gösterildiği gibi:

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
}

Bu tahsis edici işletim sisteminden bitişik bir bellek alanı ön tahsis eder. next kullanılabilir bellek adresine işaret eder ve end bellek alanının bitiş adresine işaret eder, aşağıdaki şekilde gösterildiği gibi anlaşılabilir.

Doğrusal tahsis edicinin bellek tahsis yöntemi çok kolay anlaşılır. İstenen bellek boyutuna göre yeterli kalan alan olup olmadığını kontrol eder. Eğer yeterliyse, next alanını günceller ve kalan alanın başlangıç adresini döndürür. Kod şöyledir:

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

Bu tahsis yönteminin avantajı hızlı ve basit olmasıdır. Dezavantajı da oldukça belirgindir: serbest bırakılan belleği yeniden kullanamaz çünkü next alanı sadece kalan alan bellek adresine işaret eder. Önceden kullanılan ve serbest bırakılan bellek alanını algılayamaz, bu da çok fazla bellek alanı israfına neden olur, aşağıdaki şekilde gösterildiği gibi.

Bu yüzden doğrusal tahsis Go'da ana tahsis yöntemi değildir. Sadece 32-bit makinelerde bellek ön tahsis fonksiyonu olarak kullanılır.

Bağlı Tahsis

Bağlı tahsis edici runtime.fixalloc yapısına karşılık gelir. Bağlı tahsis edici tarafından tahsis edilen bellek bitişik değildir ve tek yönlü bağlı liste olarak bulunur. Bağlı tahsis edici birkaç sabit boyutlu bellek bloğundan oluşur ve her bellek bloğu birkaç sabit boyutlu bellek yığınlarından oluşur. Her seferinde bellek tahsis edildiğinde, sabit boyutlu bir bellek yığını kullanılır.

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
}

Alanları doğrusal tahsis edici kadar basit ve kolay anlaşılır değildir. İşte önemli olanların kısa tanıtımı:

  • size: Bellek tahsis edildiğinde her seferinde ne kadar bellek kullanılır.
  • list: Yeniden kullanılabilir bellek yığınlarının bağlı listesinin baş düğümüne işaret eder. Her bellek yığını boyutu size tarafından belirlenir.
  • chunk: Şu anda kullanılan bellek bloğundaki boş adrese işaret eder.
  • nchunk: Mevcut bellek bloğunda kalan kullanılabilir bayt.
  • nalloc: Bellek bloğu boyutu, sabit 16KB.
  • inuse: Şu anda kullanımda olan toplam bayt.
  • zero: Bellek bloklarını yeniden kullanırken belleği temizleyip temizlememek.

Bağlı tahsis edici mevcut bellek bloğuna ve yeniden kullanılabilir bellek yığınlarına referans tutar. Her bellek bloğu boyutu sabit 16KB'dir, bu başlatma sırasında ayarlanır.

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
}

Bellek bloklarının dağılımı aşağıdaki şekilde gösterildiği gibidir. Şekildeki bellek blokları oluşturma zamanına göre düzenlenmiştir. Gerçekte, adresleri bitişik değildir.

Bağlı tahsis edicinin her seferinde tahsis ettiği bellek boyutu da sabittir, fixalloc.size tarafından belirlenir. Tahsis sırasında, ilk olarak yeniden kullanılabilir bellek blokları olup olmadığını kontrol eder. Varsa, öncelikli olarak yeniden kullanılabilir bellek bloklarını kullanır, sonra mevcut bellek bloğunu kullanır. Mevcut bellek bloğunun kalan alanı容纳 etmek için yetersizse, yeni bir bellek bloğu oluşturur. Bu mantık kısmı aşağıdaki koda karşılık gelir:

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
}

Bağlı tahsis edicinin avantajı tam olarak serbest bırakılan belleği yeniden kullanabilmesidir. Belleği yeniden kullanmak için temel birim sabit boyutlu bir bellek yığınıdır, boyutu fixalloc.size tarafından belirlenir. Belleği serbest bırakırken, bağlı tahsis edici bellek yığınını boş bellek yığınları bağlı listesinin baş düğümü olarak ekler. Kod şöyledir:

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

Bellek Bileşenleri

Go'nun bellek tahsis edici principalmente aşağıdaki bileşenlerden oluşur: mspan, heaparena, mcache, mcentral ve mheap. Go'nun yığın belleğini katman katman yönetmek için birlikte çalışırlar.

mspan

runtime.mspan Go bellek tahsisindeki temel birimdir. Yapısı şöyledir:

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

mspan ve mspan next ve prev aracılığıyla çift yönlü bağlı liste olarak bağlanır. Bellek adresleri bitişik değildir. Her mspan runtime.pageSize boyutunda mspan.npages sayfayı yönetir. Genellikle, sayfa boyutu 8KB'dir. mspan.startAddr bu sayfaların başlangıç adresini kaydeder ve mspan.limit kullanılan belleğin bitiş adresini kaydeder. Her mspan sabit elemsize boyutunda elemanları saklar, bu yüzden tutabileceği eleman sayısı da sabittir. Sabit miktar nedeniyle, nesneler mspan'da bir dizi gibi dağıtılır, [0, nelems] aralığında. freeindex nesneleri saklamak için bir sonraki kullanılabilir yuvanın indeksini kaydeder. mspan'ın üç durumu vardır:

  • mSpanDead: Bellek serbest bırakıldı.
  • mSpanInUse: Yığına tahsis edildi.
  • mSpanManual: Elle yönetilen bellek kısımlarına tahsis edildi, örneğin yığın.

mspan eleman boyutunu belirleyen spanClass'tır. spanClass kendisi bir uint8 tip tamsayıdır. Yüksek yedi bit sınıf değerini 0-67 saklar ve son bit noscan'ı göstermek için kullanılır, yani işaretçi içerip içermediği.

go
type spanClass uint8

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

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

Toplam 68 farklı değeri vardır. Tüm değerler tablo formunda runtime.sizeclasses.go dosyasında saklanır. Runtime'da, spanClass kullanarak runtime.class_to_size aracılığıyla mspan nesne boyutunu alabilir ve class_to_allocnpages aracılığıyla mspan sayfa sayısını alabilir.

classmax object sizespan sizeobject counttail wastemax memory waste ratemin alignment
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

Bu değerlerin hesaplama mantığı runtime.mksizeclasses.go dosyasındaki printComment fonksiyonunda bulunabilir. Maksimum bellek israf oranı formülü:

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

Örneğin, class 2 olduğunda, maksimum bellek israf oranı:

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

class değeri 0 olduğunda, 32KB üzerindeki büyük nesneleri tahsis etmek için kullanılan spanClass'tır. Temelde, bir büyük nesne bir mspan kaplar. Bu yüzden Go'nun yığın belleği aslında farklı sabit boyutlarda birkaç mspan bağlı listesinden oluşur.

heaparena

Daha önce belirtildiği gibi, mspan birkaç sayfadan oluşur, ancak mspan sadece sayfa adreslerine referans tutar ve bu sayfaları yönetmekten sorumlu değildir. Gerçekten bu sayfa belleklerini yöneten runtime.heaparena'dır. Her heaparena birkaç sayfayı yönetir. heaparena boyutu runtime.heapArenaBytes tarafından belirlenir, genellikle 64MB. bitmap sayfadaki ilgili adresin nesne saklayıp saklamadığını tanımlamak için kullanılır. zeroedBase bu heaparena tarafından yönetilen sayfa belleğinin başlangıç adresidir. spans her sayfanın hangi mspan tarafından kullanıldığını kaydeder.

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
}

Sayfalar ve mspan kayıtları hakkındaki mantık mheap.setSpans metodunda bulunabilir, şöyledir:

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

Go yığınında, tüm sayfa belleği iki boyutlu heaparena dizisi tarafından yönetilir. mheap.arenas alanına bakın:

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

64-bit Windows platformlarında, dizinin ilk boyutu 1 << 6 ve ikinci boyutu 1 << 16'dır. 64-bit Linux platformlarında, ilk boyut 1'dir ve ikinci boyut 1 << 22'dir. Tüm heaparena'lardan oluşan bu iki boyutlu dizi Go runtime'ın sanal bellek alanını oluşturur. Genel olarak, aşağıdaki şekil gibi görünür.

heaparena'lar bitişik olsa da, yönettikleri sayfa bellekleri bitişik değildir.

mcache

mcache runtime.mcache yapısına karşılık gelir, bu eşzamanlı zamanlama makalesinde ortaya çıkmıştır. Adı mcache olsa da, aslında işlemci P'ye bağlıdır. mcache her işlemci P'deki bellek önbelleğidir, alloc mspan bağlı liste dizisini içerir. Dizi boyutu sabit 136'dır, bu tam olarak spanClass sayısının iki katıdır. Ayrıca tiny nesne önbelleği tiny vardır, tiny tiny nesne belleğinin başlangıç adresine işaret eder, tinyoffset başlangıç adresine göre boş bellek ofsetidir ve tinyAllocs kaç tane tiny nesne tahsis edildiğini gösterir. Yığın önbelleği stackcache için Yığın Bellek Tahsisi'nde öğrenebilirsiniz.

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
}

Başlatma sırasında, mcache'in alloc'ındaki bağlı listeler sadece boş bir baş düğüm runtime.emptymspan içerir, bu kullanılabilir belleği olmayan bir mspan'dır.

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
}

Sadece bellek tahsisi gerektiğinde mcentral'dan yeni bir mspan için başvurur ve orijinal boş span'ı değiştirir. Bu iş mcache.refill metodu tarafından tamamlanır. Tek çağrı girişi runtime.mallocgc fonksiyonudur. Aşağıda basitleştirilmiş kod bulunmaktadır:

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
}

mcache kullanmanın avantajı bellek tahsisinin global kilit gerektirmemesidir. Ancak, belleği yetersiz olduğunda ve mcentral'a erişmesi gerektiğinde, kilitleme hala gereklidir.

mcentral

runtime.mcentral yığında küçük nesneleri saklayan tüm mspan'ı yönetir. mcache bellek için başvurduğunda, mcentral tarafından tahsis edilir.

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

mcentral'ın birkaç alanı vardır. spanClass saklanan mspan tipini gösterir. partial ve full iki spanSet'tir. İlki boş belleği olan mspan'ı saklar ve ikincisi boş belleği olmayan mspan'ı saklar. mcentral doğrudan mheap yığını tarafından yönetilir. Runtime'da, toplam 136 mcentral vardır.

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

mcentral principalmente iki görevden sorumludur: bellek yeterli olduğunda mcache'e kullanılabilir mspan tahsis etmek ve bellek yetersiz olduğunda mheap'ten yeni bir mspan için başvurmak. mspanmcache'e tahsis etme işi mcentral.cacheSpan metodu tarafından tamamlanır. İlk olarak, boş listenin süpürülmüş setinde mevcut mspan bulmaya çalışır:

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

Bulunamazsa, boş listenin süpürülmemiş setinde mevcut mspan arar:

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

Hala bulunamazsa, boş olmayan listenin süpürülmemiş setinde arar:

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

Sonunda hala bulunamazsa, mcentral.grow metodu mheap'ten yeni bir mspan için başvurur:

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

Normal koşullarda, bir şekilde mevcut bir mspan döndürülecektir:

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

  return s

mheap'ten mspan için başvurma süreci için, aslında mheap.alloc metodunu çağırır, bu yeni bir mspan döndürür:

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
}

Uygun şekilde başlattıktan sonra, mcache tarafından kullanım için tahsis edilebilir.

mheap

runtime.mheap Go dili yığın belleğinin yöneticisidir. Runtime'da, global değişken runtime.mheap_ olarak bulunur:

go
var mheap_ mheap

Tüm oluşturulan mspan'ı, tüm mcentral'ı ve tüm heaparena'yı yönetir, ayrıca birçok diğer çeşitli tahsis ediciyi de yönetir. Basitleştirilmiş yapısı şöyledir:

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
}

mheap için, runtime'da principalmente dört görev vardır:

  • Yığını başlatmak
  • mspan tahsis etmek
  • mspan serbest bırakmak
  • Yığın genişletme

Bu dört şeyi sırayla tartışalım.

Başlatma

Yığın başlatma programın önyükleme aşamasında gerçekleşir, bu da zamanlayıcının başlatma aşamasıdır. Çağrı sırası:

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

Başlatma sırasında, principalmente çeşitli tahsis edicilerin başlatma işini yürütmekten sorumludur:

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

Bu mspan tahsis etmekten sorumlu tahsis edici mheap.spanalloc, sayfa tahsisi için sorumlu tahsis edici mheap.pages ve tüm mcentral'ın başlatmasını içerir.

Tahsis

mheap'te, mspan tahsisi mheap.allocSpan metodu tarafından tamamlanır:

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

İstenen bellek yeterince küçükse, yani npages < pageCachePages/4 koşulunu sağlıyorsa, kilitleme olmadan yerel P'nin mspan önbelleğinden mevcut bir mspan almaya çalışır. P'nin önbelleği boşsa, ilk olarak onu başlatacaktır:

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

Sonra P önbelleğinden alır, mheap.tryAllocMSpan metodu tarafından tamamlanır:

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

P önbelleğinden mspan alma kodu şöyledir. Önbellekteki son mspan'ı almaya çalışır:

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
}

İstenen bellek nispeten büyükse, yığında tahsis edilecektir. Bu süreç bir kilit tutmayı gerektirir:

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)

İlk olarak, yeterli sayfa belleğini tahsis etmek için pageAlloc.alloc kullanır. Eğer yığın belleği yetersizse, mheap.grow genişletme yapar. Sayfa belleği tahsisi tamamlandıktan sonra, bağlı tahsis edici mheap.spanalloc 64 mspan'ı P'nin yerel önbelleğine tahsis eder (64 tam olarak önbellek dizi uzunluğunun yarısıdır), sonra P'nin önbelleğinden mevcut bir mspan döndürür:

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
}

Yukarıdaki iki duruma göre, nihayetinde mevcut bir mspan elde edilebilir. Son olarak, mspan'ı başlattıktan sonra, döndürülebilir:

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

Serbest Bırakma

mspan bağlı tahsis edici tarafından tahsis edildiğinden, doğal olarak belleği serbest bırakırken, aynı zamanda bağlı tahsis edici tarafından serbest bırakılır:

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

İlk olarak, sayfa tahsis edici mheap.pages aracılığıyla belirtilen sayfa belleğini serbest bırakıldı olarak işaretler, sonra mspan'ın durumunu mSpanDead olarak ayarlar ve son olarak mheap.spanalloc tahsis edicisi mspan'ı serbest bırakır:

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

Eğer P önbelleği dolu değilse, P'nin yerel önbelleğine konur ve kullanılmaya devam eder. Aksi takdirde, yığın belleğine geri serbest bırakılır.

Genişletme

heaparena tarafından yönetilen sayfa bellek alanı başlangıçta tamamen tahsis edilmez. Sadece bellek gerektiğinde tahsis edilir. Yığın belleğini genişletmekten sorumlu olan mheap.grow metodudur. Aşağıda basitleştirilmiş kod bulunmaktadır:

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

İlk olarak, npage'e göre gereken belleği hesaplar ve hizalar, sonra mevcut heaparena'nın yeterli belleği olup olmadığını belirler. Yeterli değilse, mheap.sysAlloc mevcut heaparena için daha fazla bellek için başvurur veya yeni bir heaparena tahsis eder:

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

İlk olarak, ön tahsis edilen bellek alanında bir bellek parçası için doğrusal tahsis edici mheap.arena kullanmayı dener. Başarısız olursa, hintList'e göre genişletir. hintList'in tipi runtime.arenaHint'tir, bu özellikle heaparena genişletme ile ilgili adres bilgilerini kaydeder:

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

Bellek başvurusu tamamlandıktan sonra, arenas iki boyutlu dizisine güncellenir:

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

Son olarak, sayfa tahsis edici bu bellek parçasını hazır olarak işaretler:

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

Nesne Tahsisi

Go nesneler için bellek tahsis ettiğinde, boyuta göre üç farklı tipe ayırır:

  • Tiny nesneler - tiny, 16B'den az
  • Küçük nesneler - small, 32KB'den az
  • Büyük nesneler - large, 32KB'den büyük

Bu üç farklı tipe göre, bellek tahsisi sırasında farklı mantık yürütülür. Nesne belleğini tahsis etmekten sorumlu fonksiyon runtime.mallocgc'dir, aşağıdaki fonksiyon imzasına sahiptir:

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

Sadece üç parametresi vardır: bellek boyutu, tipi ve belleğin temizlenmesi gerekip gerekmediğini belirten bir boolean değer. Tüm Go nesne bellek tahsisinin giriş fonksiyonudur. new fonksiyonunu işaretçi oluşturmak için kullanırken, bu fonksiyona girer. Bellek tahsisi başarılı olduğunda, döndürülen işaretçi nesnenin adresidir. mspan bölümünde, her mspan'ın bir spanClass'a sahip olduğu belirtildi. spanClass mspan'ın sabit boyutunu belirler. Go [0, 32KB] aralığını 68 farklı boyuta böler. Bu yüzden Go belleği farklı sabit boyutlarda birkaç mspan bağlı listesinden oluşur. Nesne belleğini tahsis ederken, sadece nesne boyutuna göre ilgili spanClass'ı hesaplayın, sonra spanClass'a göre ilgili mspan bağlı listesini bulun ve son olarak bağlı listeden mevcut mspan arayın. Bu hiyerarşik yaklaşım bellek parçalanma problemini daha etkili çözebilir.

Tiny Nesneler

16B'den az tüm işaretçi olmayan tiny nesneler P'deki tiny tahsis edici tarafından aynı bitişik belleğe tahsis edilir. runtime.mcache'te, tiny alanı bu belleğin temel adresini kaydeder:

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

Tiny nesnelerin boyutu runtime.maxTinySize sabiti tarafından belirlenir, bu 16B'dir. Tiny nesneleri saklamak için kullanılan bellek bloğu da bu boyuttadır. Genellikle, burada saklanan nesneler bazı küçük dizelerdir. Tiny nesneleri tahsis etmekten sorumlu kod kısmı şöyledir:

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

Eğer mevcut tiny bellek bloğunun容纳 etmek için yeterli alanı varsa, yani off+size <= maxTinySize, doğrudan mevcut bellek bloğunu kullanır. Yeterli değilse, ilk olarak mcache'in span önbelleğinden mevcut alan almaya çalışır. Eğer bu da işe yaramazsa, mcentral'dan bir mspan için başvurur. Her ne olursa olsun, nihayetinde mevcut bir adres elde edecektir ve son olarak eski tiny nesne bellek bloğunu yeni ile değiştirir.

Küçük Nesneler

Go dili runtime'ındaki çoğu nesne [16B, 32KB] aralığındaki küçük nesnelerdir. Küçük nesne tahsis süreci en sorunludur, ancak kod en azdır. Küçük nesne tahsisinden sorumlu kod kısmı şöyledir:

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

İlk olarak, nesnenin boyutuna göre hangi spanClass kategorisinin kullanılması gerektiğini hesaplar. Sonra runtime.nextFreeFast spanClass'a göre mcache'teki ilgili önbellek mspan'dan mevcut bellek alanı almaya çalışır:

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
}

mspan.allocCache'in rolü bellek alanının nesneler tarafından kullanılıp kullanılmadığını kaydetmektir. Belleği uzay boyutuna göre değil, nesne sayısına göre birer birer böler. Bu mspan'ı bir nesne dizisi olarak görmeye eşdeğerdir, aşağıdaki şekilde gösterildiği gibi.

allocCache 64-bit bir sayıdır. Her bit bir bellek alanı parçasına karşılık gelir. Eğer bir bit 0 ise, belleğin bir nesne tarafından kullanıldığını gösterir. Eğer 1 ise, belleğin boş olduğunu gösterir. sys.TrailingZeros64(s.allocCache)'in amacı sondaki sıfır sayısını hesaplamaktır. Eğer sonuç 64 ise, mevcut boş bellek olmadığını gösterir. Varsa, boş bellek ofsetini hesaplar, mspan'ın temel adresini ekler ve döndürür.

mcache'te yeterli alan olmadığında, mcentral'a başvurur. Bu iş mcache.nextFree metodu tarafından tamamlanır:

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
}

İçindeki mcache.refill mcentral'dan mevcut bir mspan için başvurudan sorumludur:

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

mcentral.cacheSpan metodu bellek yetersiz olduğunda mcentral.grow aracılığıyla genişletme yapar. Genişletme mheap'ten yeni mspan için başvurur:

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

Bu yüzden sonunda, küçük nesne bellek tahsisi seviye seviye gider: ilk mcache, sonra mcentral ve son olarak mheap. mcache tahsis maliyeti en düşüktür çünkü P'nin yerel önbelleğidir ve bellek tahsis etmek bir kilit tutmayı gerektirmez. mcentral bir sonraktır. Doğrudan mheap'ten bellek için başvurmak en yüksek maliyete sahiptir çünkü mheap.alloc metodu tüm yığının global kilidi için yarışır.

Büyük Nesneler

Büyük nesne tahsisi en basitidir. Eğer nesne boyutu 32KB'yi aşarsa, doğrudan mheap'ten yeni bir mspan için başvurur ve容纳 eder. Büyük nesne tahsisinden sorumlu kod kısmı şöyledir:

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

Bunlar arasında, mcache.allocLarge mheap'ten büyük nesne bellek alanı için başvurudan sorumludur:

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

Koddan görülebilir ki, büyük nesneler 0 spanClass değeri kullanır. Temelde, bir büyük nesne bir mspan kaplar.

Diğerleri

Bellek İstatistikleri

Go runtime kullanıcılara ReadMemStats fonksiyonunu açığa çıkarır, bu runtime bellek koşullarını统计 etmek için kullanılabilir:

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

Ancak bunu kullanmanın maliyeti çok yüksektir. Koddan görülebilir ki, bellek koşullarını analiz etmeden önce STW gereklidir. STW süresi birkaç milisaniyeden yüzlerce milisaniyeye kadar değişebilir. Genellikle sadece hata ayıklama ve sorun giderme sırasında kullanılır. runtime.MemStats yapısı yığın belleği, yığın belleği ve GC ile ilgili bilgileri kaydeder:

go
type MemStats struct {
    // Overall statistics
    Alloc uint64
    TotalAlloc uint64
    Sys uint64
    Lookups uint64
    Mallocs uint64
    Frees uint64

    // Heap memory statistics
    HeapAlloc uint64
    HeapSys uint64
    HeapIdle uint64
    HeapInuse uint64
    HeapReleased uint64
    HeapObjects uint64

    // Stack memory statistics
    StackInuse uint64
    StackSys uint64

    // Memory component statistics
    MSpanInuse uint64
    MSpanSys uint64
    MCacheInuse uint64
    MCacheSys uint64
    BuckHashSys uint64

    // GC related statistics
    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

Bellek tahsis edici açıkça yığın belleğini tahsis etmek için kullanılır, ancak yığın iki parçaya ayrılır: bir parçası Go runtime'ının kendi ihtiyaç duyduğu yığın belleğidir ve diğer parçası kullanıcılara açık yığın belleğidir. Bu yüzden bazı yapılarda, böyle gömülü alanlar görebilirsiniz:

go
_ sys.NotInHeap

Bu tipin belleğinin kullanıcı yığınına tahsis edilmeyeceğini gösterir. Bu tür gömülü alan özellikle bellek tahsis bileşenlerinde yaygındır, örneğin kullanıcı yığınını temsil eden yapı runtime.mheap:

go
type mheap struct {
  _ sys.NotInHeap
}

sys.NotInHeap'in gerçek rolü runtime verimliliğini artırmak için bellek bariyerlerinden kaçınmaktır, kullanıcı yığını GC çalıştırmalıdır bu yüzden bellek bariyerlerine ihtiyaç duyar.

Golang by www.golangdev.cn edit