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:
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:
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.
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ı boyutusizetarafı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.
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:
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:
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:
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.
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.
| class | max object size | span size | object count | tail waste | max memory waste rate | min alignment |
|---|---|---|---|---|---|---|
| 1 | 8 | 8192 | 1024 | 0 | 87.50% | 8 |
| 2 | 16 | 8192 | 512 | 0 | 43.75% | 16 |
| 3 | 24 | 8192 | 341 | 8 | 29.24% | 8 |
| 4 | 32 | 8192 | 256 | 0 | 21.88% | 32 |
| 5 | 48 | 8192 | 170 | 32 | 31.52% | 16 |
| 6 | 64 | 8192 | 128 | 0 | 23.44% | 64 |
| 7 | 80 | 8192 | 102 | 32 | 19.07% | 16 |
| 8 | 96 | 8192 | 85 | 32 | 15.95% | 32 |
| 9 | 112 | 8192 | 73 | 16 | 13.56% | 16 |
| 10 | 128 | 8192 | 64 | 0 | 11.72% | 128 |
| 11 | 144 | 8192 | 56 | 128 | 11.82% | 16 |
| 12 | 160 | 8192 | 51 | 32 | 9.73% | 32 |
| 13 | 176 | 8192 | 46 | 96 | 9.59% | 16 |
| 14 | 192 | 8192 | 42 | 128 | 9.25% | 64 |
| 15 | 208 | 8192 | 39 | 80 | 8.12% | 16 |
| 16 | 224 | 8192 | 36 | 128 | 8.15% | 32 |
| 17 | 240 | 8192 | 34 | 32 | 6.62% | 16 |
| 18 | 256 | 8192 | 32 | 0 | 5.86% | 256 |
| 19 | 288 | 8192 | 28 | 128 | 12.16% | 32 |
| 20 | 320 | 8192 | 25 | 192 | 11.80% | 64 |
| 21 | 352 | 8192 | 23 | 96 | 9.88% | 32 |
| 22 | 384 | 8192 | 21 | 128 | 9.51% | 128 |
| 23 | 416 | 8192 | 19 | 288 | 10.71% | 32 |
| 24 | 448 | 8192 | 18 | 128 | 8.37% | 64 |
| 25 | 480 | 8192 | 17 | 32 | 6.82% | 32 |
| 26 | 512 | 8192 | 16 | 0 | 6.05% | 512 |
| 27 | 576 | 8192 | 14 | 128 | 12.33% | 64 |
| 28 | 640 | 8192 | 12 | 512 | 15.48% | 128 |
| 29 | 704 | 8192 | 11 | 448 | 13.93% | 64 |
| 30 | 768 | 8192 | 10 | 512 | 13.94% | 256 |
| 31 | 896 | 8192 | 9 | 128 | 15.52% | 128 |
| 32 | 1024 | 8192 | 8 | 0 | 12.40% | 1024 |
| 33 | 1152 | 8192 | 7 | 128 | 12.41% | 128 |
| 34 | 1280 | 8192 | 6 | 512 | 15.55% | 256 |
| 35 | 1408 | 16384 | 11 | 896 | 14.00% | 128 |
| 36 | 1536 | 8192 | 5 | 512 | 14.00% | 512 |
| 37 | 1792 | 16384 | 9 | 256 | 15.57% | 256 |
| 38 | 2048 | 8192 | 4 | 0 | 12.45% | 2048 |
| 39 | 2304 | 16384 | 7 | 256 | 12.46% | 256 |
| 40 | 2688 | 8192 | 3 | 128 | 15.59% | 128 |
| 41 | 3072 | 24576 | 8 | 0 | 12.47% | 1024 |
| 42 | 3200 | 16384 | 5 | 384 | 6.22% | 128 |
| 43 | 3456 | 24576 | 7 | 384 | 8.83% | 128 |
| 44 | 4096 | 8192 | 2 | 0 | 15.60% | 4096 |
| 45 | 4864 | 24576 | 5 | 256 | 16.65% | 256 |
| 46 | 5376 | 16384 | 3 | 256 | 10.92% | 256 |
| 47 | 6144 | 24576 | 4 | 0 | 12.48% | 2048 |
| 48 | 6528 | 32768 | 5 | 128 | 6.23% | 128 |
| 49 | 6784 | 40960 | 6 | 256 | 4.36% | 128 |
| 50 | 6912 | 49152 | 7 | 768 | 3.37% | 256 |
| 51 | 8192 | 8192 | 1 | 0 | 15.61% | 8192 |
| 52 | 9472 | 57344 | 6 | 512 | 14.28% | 256 |
| 53 | 9728 | 49152 | 5 | 512 | 3.64% | 512 |
| 54 | 10240 | 40960 | 4 | 0 | 4.99% | 2048 |
| 55 | 10880 | 32768 | 3 | 128 | 6.24% | 128 |
| 56 | 12288 | 24576 | 2 | 0 | 11.45% | 4096 |
| 57 | 13568 | 40960 | 3 | 256 | 9.99% | 256 |
| 58 | 14336 | 57344 | 4 | 0 | 5.35% | 2048 |
| 59 | 16384 | 16384 | 1 | 0 | 12.49% | 8192 |
| 60 | 18432 | 73728 | 4 | 0 | 11.11% | 2048 |
| 61 | 19072 | 57344 | 3 | 128 | 3.57% | 128 |
| 62 | 20480 | 40960 | 2 | 0 | 6.87% | 4096 |
| 63 | 21760 | 65536 | 3 | 256 | 6.25% | 256 |
| 64 | 24576 | 24576 | 1 | 0 | 11.45% | 8192 |
| 65 | 27264 | 81920 | 3 | 128 | 10.00% | 128 |
| 66 | 28672 | 57344 | 2 | 0 | 4.91% | 4096 |
| 67 | 32768 | 32768 | 1 | 0 | 12.50% | 8192 |
Bu değerlerin hesaplama mantığı runtime.mksizeclasses.go dosyasındaki printComment fonksiyonunda bulunabilir. Maksimum bellek israf oranı formülü:
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.4375class 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.
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:
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:
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.
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.
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:
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.
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.
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. mspan'ı mcache'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:
// 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:
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:
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:
s = c.grow()
if s == nil {
return nil
}Normal koşullarda, bir şekilde mevcut bir mspan döndürülecektir:
havespan:
freeByteBase := s.freeindex &^ (64 - 1)
whichByte := freeByteBase / 8
// Init alloc bits cache.
s.refillAllocCache(whichByte)
s.allocCache >>= s.freeindex % 64
return smheap'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:
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:
var mheap_ mheapTü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:
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
mspantahsis etmekmspanserbest 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ı:
schedinit() -> mallocinit() -> mheap_.init()Başlatma sırasında, principalmente çeşitli tahsis edicilerin başlatma işini yürütmekten sorumludur:
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:
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:
// 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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
// Update the page allocator's structures to make this
// space ready for allocation.
h.pages.grow(v, nBase-v)
totalGrowth += nBase - vNesne 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:
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.PointerSadece üç 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:
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:
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 = maxTinySizeEğ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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
_ sys.NotInHeapBu 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:
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.
