memory
Im Gegensatz zu traditionellen C/C++ ist Go eine Sprache mit Garbage Collection. In den meisten Fällen wird die Zuweisung und Freigabe von Speicher automatisch von Go verwaltet. Ob der Speicher eines Objekts auf dem Stack oder dem Heap zugewiesen wird, entscheidet der Compiler. Im Grunde braucht der Benutzer nicht an der Speicherverwaltung teilzunehmen - er muss lediglich den Speicher verwenden. In Go besteht die Heap-Speicherverwaltung aus zwei großen Komponenten: Der Speicherzuweiser ist für die Zuweisung des Heap-Speichers zuständig, und der Garbage Collector ist für das Zurückfordern und Freigeben von nicht mehr verwendetem Heap-Speicher verantwortlich. Dieser Artikel beschreibt hauptsächlich die Arbeitsweise des Speicherzuweisers. Go's Speicherzuweiser ist stark von Google's TCMalloc-Speicherzuweiser beeinflusst.
Zuweiser
In Go gibt es zwei Arten von Speicherzuweisern: den linearen Zuweiser und den verketteten Zuweiser.
Lineare Zuweisung
Der lineare Zuweiser entspricht der Struktur runtime.linearAlloc, wie unten gezeigt:
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
}Dieser Zuweiser reserviert im Voraus einen zusammenhängenden Speicherbereich vom Betriebssystem. next zeigt auf die verwendbare Speicheradresse, und end zeigt auf die Endadresse des Speicherbereichs. Dies kann grob wie in der folgenden Abbildung verstanden werden.

Die Speicherzuweisungsmethode des linearen Zuweisers ist sehr einfach zu verstehen: Basierend auf der angeforderten Speichergröße wird geprüft, ob genügend verbleibender Platz vorhanden ist. Wenn ja, wird das Feld next aktualisiert und die Startadresse des verbleibenden Platzes zurückgegeben, wie im folgenden Code gezeigt.
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)
}Der Vorteil dieser Zuweisungsmethode ist Geschwindigkeit und Einfachheit. Der Nachteil ist jedoch offensichtlich: Bereits freigegebener Speicher kann nicht wiederverwendet werden, da next nur auf den verbleibenden Speicherplatz zeigt. Für zuvor verwendeten und dann freigegebenen Speicherplatz besteht keine Wahrnehmung, was zu erheblicher Speicherverschwendung führt, wie in der folgenden Abbildung gezeigt.

Daher ist die lineare Zuweisung nicht die Hauptzuweisungsmethode in Go. Sie wird nur auf 32-Bit-Maschinen als Funktion zur Speichervorabzuweisung verwendet.
Verkettete Zuweisung
Der verkettete Zuweiser entspricht der Struktur runtime.fixalloc. Der vom verketteten Zuweiser zugewiesene Speicher ist nicht zusammenhängend, sondern existiert in Form einer einfach verketteten Liste. Der verkettete Zuweiser besteht aus mehreren Speicherblöcken fester Größe, wobei jeder Speicherblock aus mehreren Speicherscheiben fester Größe besteht. Bei jeder Speicherzuweisung wird eine Speicherscheibe fester Größe verwendet.
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
}Seine Felder sind nicht so einfach zu verstehen wie die des linearen Zuweisers. Hier eine kurze Einführung der wichtigsten:
size: Gibt an, wie viel Speicher bei jeder Speicherzuweisung verwendet wird.list: Zeigt auf den Kopfknoten der wiederverwendbaren Speicherscheiben. Die Größe jedes Speicherplatzes wird durchsizebestimmt.chunk: Zeigt auf die freie Adresse im aktuell verwendeten Speicherblocknchunk: Verbleibende verfügbare Bytes im aktuellen Speicherblocknalloc: Größe des Speicherblocks, fest auf 16KB.inuse: Gesamtzahl der bereits verwendeten Byteszero: Ob der Speicher beim Wiederverwenden des Speicherblocks auf Null gesetzt werden soll
Der verkettete Zuweiser hält Referenzen auf den aktuellen Speicherblock und die wiederverwendbaren Speicherscheiben. Jeder Speicherblock hat eine feste Größe von 16KB, die bei der Initialisierung festgelegt wird.
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
}Die Verteilung der Speicherblöcke ist in der folgenden Abbildung dargestellt. Die Speicherblöcke sind nach ihrer Erstellungszeit angeordnet, aber ihre Adressen sind nicht zusammenhängend.

Der verkettete Zuweiser weist bei jeder Zuweisung Speicher fester Größe zu, die durch fixalloc.size bestimmt wird. Bei der Zuweisung wird zuerst geprüft, ob wiederverwendbare Speicherblöcke vorhanden sind. Falls ja, werden diese bevorzugt verwendet. Erst dann wird der aktuelle Speicherblock verwendet. Wenn der verbleibende Platz im aktuellen Speicherblock nicht ausreicht, wird ein neuer Speicherblock erstellt. Diese Logik entspricht dem folgenden Code.
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
}Der Vorteil des verketteten Zuweisers ist gerade, dass er freigegebenen Speicher wiederverwenden kann. Die Grundeinheit für die Speicherwiederverwendung ist eine Speicherscheibe fester Größe, deren Größe durch fixalloc.size bestimmt wird. Beim Freigeben von Speicher fügt der verkettete Zuweiser diese Speicherscheibe als Kopfknoten zur Liste der freien Speicherscheiben hinzu, wie im folgenden Code gezeigt:
func (f *fixalloc) free(p unsafe.Pointer) {
f.inuse -= f.size
v := (*mlink)(p)
v.next = f.list
f.list = v
}Speicherkomponenten
Der Speicherzuweiser in Go besteht hauptsächlich aus den Komponenten mspan, heaparena, mcache, mcentral und mheap. Sie wirken zusammen und verwalten den gesamten Heap-Speicher von Go.
mspan

runtime.mspan ist die Grundeinheit der Go-Speicherzuweisung. Ihre Struktur ist wie folgt:
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 ist durch next und prev als doppelt verkettete Liste mit anderen mspan verknüpft, wobei die Speicheradressen nicht zusammenhängend sind. Jeder mspan verwaltet mspan.npages Seiten mit der Größe runtime.pageSize. Normalerweise beträgt die Seitengröße 8KB. mspan.startAddr zeichnet die Startadresse dieser Seiten auf und mspan.limit zeichnet die Endadresse des verwendeten Speichers auf. Die Größe der in jedem mspan gespeicherten Elemente elemsize ist fest, daher ist auch die Anzahl der Elemente, die aufgenommen werden können, fest. Da die Anzahl fest ist, werden die Objekte wie ein Array im mspan verteilt, im Bereich [0, nelems]. Gleichzeitig zeichnet freeindex den Index für das nächste speicherbare Objekt auf. mspan hat insgesamt drei Zustände:
- mSpanDead: Speicher wurde bereits freigegeben
- mSpanInUse: Wurde dem Heap zugewiesen
- mSpanManual: Wurde dem Bereich für manuelle Speicherverwaltung zugewiesen, zum Beispiel dem Stack.
Was die Größe der mspan-Elemente bestimmt, ist spanClass. spanClass selbst ist eine uint8-Ganzzahl, wobei die oberen 7 Bits einen Class-Wert von 0-67 speichern und das letzte Bit noscan angibt, also ob Zeiger enthalten sind.
type spanClass uint8
func (sc spanClass) sizeclass() int8 {
return int8(sc >> 1)
}
func (sc spanClass) noscan() bool {
return sc&1 != 0
}Es gibt insgesamt 68 verschiedene Werte, die alle tabellarisch in der Datei runtime.sizeclasses.go gespeichert sind. Zur Laufzeit kann man mit spanClass über runtime.class_to_size die Objektgröße des mspan erhalten und über class_to_allocnpages die Seitenanzahl des mspan.
| class | Maximale Objektgröße | Span-Größe | Objektanzahl | Endverschwendung | Maximale Speicherverschwendung | Minimale Ausrichtung |
|---|---|---|---|---|---|---|
| 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 |
Die Berechnungslogik für diese Werte kann in der Funktion printComment in runtime.mksizeclasses.go gefunden werden. Die Formel für die maximale Speicherverschwendung lautet:
float64((size-prevSize-1)*objects+tailWaste) / float64(spanSize)Wenn der class-Wert 0 ist, ist dies der spanClass, der speziell für die Zuweisung großer Objekte über 32KB verwendet wird. Grundsätzlich belegt ein großes Objekt einen mspan. Daher besteht der Go-Heap-Speicher tatsächlich aus mehreren mspan unterschiedlicher fester Größen.
heaparena

Wie oben erwähnt, besteht mspan aus mehreren Seiten, aber mspan hält nur die Adressreferenzen der Seiten und ist nicht für deren Verwaltung zuständig. Für die Verwaltung dieser Seitenspeicher ist runtime.heaparena verantwortlich. Jedes heaparena verwaltet mehrere Seiten. Die Größe von heaparena wird durch runtime.heapArenaBytes bestimmt, normalerweise 64MB. bitmap wird verwendet, um zu kennzeichnen, ob an der entsprechenden Adresse in der Seite ein Objekt gespeichert ist. zeroedBase ist die Startadresse des von diesem heaparena verwalteten Seitenspeichers, und spans zeichnet auf, welcher mspan jede Seite verwendet.
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
}Die Logik zur Aufzeichnung von Seiten und mspan kann in der Methode mheap.setSpans gefunden werden:
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
}
}Im Go-Heap wird der gesamte Seitenspeicher von einem zweidimensionalen heaparena-Array verwaltet, siehe das Feld mheap.arenas.
type mheap struct {
arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
}Auf 64-Bit-Windows-Plattformen ist die erste Dimension des Arrays 1 << 6, die zweite Dimension ist 1 << 16. Auf 64-Bit-Linux-Plattformen ist die erste Dimension 1 und die zweite Dimension ist 1 << 22. Dieses zweidimensionale Array aus allen heaparena bildet den virtuellen Speicherplatz der Go-Laufzeitumgebung, wie in der folgenden Abbildung gezeigt.

Obwohl die heaparena benachbart sind, ist der von ihnen verwaltete Seitenspeicher nicht zusammenhängend.
mcache
mcache entspricht der Struktur runtime.mcache. Obwohl sein Name mcache lautet, ist er tatsächlich mit dem Prozessor P verknüpft. mcache ist der Speichercache auf jedem Prozessor P. Er enthält die mspan-Listen-Array alloc mit einer festen Größe von 136, was genau der doppelten Anzahl von spanClass entspricht. Dazu kommt der Mikroobjekt-Cache tiny, wobei tiny auf die Startadresse des Mikroobjektspeichers zeigt, tinyoffset der Offset des freien Speichers relativ zur Startadresse ist und tinyAllocs angibt, wie viele Mikroobjekte zugewiesen wurden. Informationen zum Stack-Cache stackcached finden Sie im Abschnitt Stack-Speicherzuweisung.
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
}Bei der Initialisierung enthalten alle Listen in alloc von mcache nur einen leeren Kopfknoten runtime.emptymspan, also einen mspan ohne verfügbaren Speicher.
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
}Erst wenn eine Speicherzuweisung erforderlich ist, wird ein neuer mspan von mcentral angefordert, um den ursprünglichen leeren Span zu ersetzen. Diese Arbeit wird von der Methode mcache.refill erledigt. Ihr einziger Aufrufpunkt ist die Funktion runtime.mallocgc. Hier ist der vereinfachte Code:
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
}Der Vorteil der Verwendung von mcache liegt darin, dass bei der Speicherzuweisung keine globale Sperre benötigt wird. Wenn jedoch der Speicher nicht ausreicht, muss auf mcentral zugegriffen werden, wobei weiterhin eine Sperre erforderlich ist.
mcentral
runtime.mcentral verwaltet alle mspan im Heap, die kleine Objekte enthalten. Bei der Speicheranforderung von mcache wird ebenfalls von mcentral zugewiesen.
type mcentral struct {
_ sys.NotInHeap
spanclass spanClass
partial [2]spanSet
full [2]spanSet
}mcentral hat wenige Felder. spanClass gibt den Typ der gespeicherten mspan an. partial und full sind zwei spanSet: Ersteres speichert mspan mit freiem Speicher, Letzteres speichert mspan ohne freien Speicher. mcentral wird direkt vom mheap-Heap verwaltet. Zur Laufzeit gibt es insgesamt 136 mcentral.
type mheap struct {
central [numSpanClasses]struct {
mcentral mcentral
pad [(cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize) % cpu.CacheLinePadSize]byte
}
}mcentral ist hauptsächlich für zwei Aufgaben verantwortlich: Wenn genügend Speicher vorhanden ist, wird ein verfügbarer mspan an mcache zugewiesen. Wenn nicht genügend Speicher vorhanden ist, wird ein neuer mspan von mheap angefordert. Die Zuweisung von mspan an mcache erfolgt durch die Methode mcentral.cacheSpan. Zuerst wird in der bereits bereinigten Menge der Freiliste nach einem verfügbaren mspan gesucht.
// Try partial swept spans first.
sg := mheap_.sweepgen
if s = c.partialSwept(sg).pop(); s != nil {
goto havespan
}Wenn keiner gefunden wird, wird in der nicht bereinigten Menge der Freiliste gesucht:
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
}
}Wenn immer noch nichts gefunden wurde, wird in der nicht bereinigten Menge der Nicht-Freiliste gesucht:
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)
}
}Wenn schließlich immer noch nichts gefunden wurde, wird durch die Methode mcentral.grow ein neuer mspan von mheap angefordert:
s = c.grow()
if s == nil {
return nil
}Normalerweise wird in jedem Fall ein verfügbarer mspan zurückgegeben:
havespan:
freeByteBase := s.freeindex &^ (64 - 1)
whichByte := freeByteBase / 8
// Init alloc bits cache.
s.refillAllocCache(whichByte)
s.allocCache >>= s.freeindex % 64
return sFür den Prozess der mspan-Anforderung von mheap wird tatsächlich die Methode mheap.alloc aufgerufen, die einen neuen mspan zurückgibt:
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
}Nach der Initialisierung kann er an mcache zur Verwendung zugewiesen werden.
mheap

runtime.mheap ist der Verwalter des Go-Heap-Speichers. Zur Laufzeit existiert er als globale Variable runtime.mheap_.
var mheap_ mheapEr verwaltet alle erstellten mspan, alle mcentral und alle heaparena sowie viele andere verschiedene Zuweiser. Seine vereinfachte Struktur ist wie folgt:
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
}Für mheap gibt es zur Laufzeit hauptsächlich vier Aufgaben:
- Heap initialisieren
mspanzuweisenmspanfreigeben- Heap erweitern
Im Folgenden werden diese vier Aufgaben der Reihe nach erläutert.
Initialisierung
Die Initialisierung des Heaps erfolgt in der Boot-Phase des Programms, gleichzeitig mit der Initialisierung des Schedulers. Die Aufrufreihenfolge ist:
schedinit() -> mallocinit() -> mheap_.init()Während der Initialisierung ist er hauptsächlich für die Ausführung der Initialisierungsarbeiten der verschiedenen Zuweiser zuständig:
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)
}Dazu gehören der für die Zuweisung von mspan zuständige Zuweiser mheap.spanalloc, der für die Seitenzuweisung zuständige Zuweiser mheap.pages sowie die Initialisierung aller mcentral.
Zuweisung
In mheap wird die Zuweisung von mspan durch die Methode mheap.allocSpan erledigt:
func (h *mheap) allocSpan(npages uintptr, typ spanAllocType, spanclass spanClass) (s *mspan)Wenn der angeforderte Speicher klein genug ist, d.h. npages < pageCachePages/4 erfüllt, wird versucht, ohne Sperre einen verfügbaren mspan aus dem lokalen P-Cache zu holen. Wenn der P-Cache leer ist, wird er zuerst initialisiert:
// If the cache is empty, refill it.
if c.empty() {
lock(&h.lock)
*c = h.pages.allocToCache()
unlock(&h.lock)
}Dann wird aus dem P-Cache geholt, was durch die Methode mheap.tryAllocMSpan erledigt wird:
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
}
}
}Der Code zum Holen von mspan aus dem P-Cache sieht wie folgt aus. Er versucht, den letzten mspan im Cache zu holen:
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
}Wenn der angeforderte Speicher groß ist, wird Speicher auf dem Heap zugewiesen. Dieser Prozess erfordert das Halten einer Sperre:
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)Zuerst wird pageAlloc.alloc verwendet, um genügend Seitenspeicher zuzuweisen. Wenn der Heap-Speicher nicht ausreicht, wird mheap.grow zum Erweitern aufgerufen. Nach Abschluss der Seitenspeicherzuweisung werden durch den verketteten Zuweiser mheap.spanalloc 64 mspan dem lokalen P-Cache zugewiesen. 64 ist genau die Hälfte der Cache-Array-Länge. Dann wird ein verfügbarer mspan aus dem P-Cache zurückgegeben:
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
}In beiden oben genannten Fällen wird schließlich ein verfügbarer mspan erhalten. Nach der Initialisierung des mspan kann er zurückgegeben werden:
HaveSpan:
h.initSpan(s, typ, spanclass, base, npages)
return sFreigabe
Da mspan durch den verketteten Zuweiser zugewiesen wird, wird er beim Freigeben von Speicher auch durch diesen freigegeben:
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)
}Zuerst wird durch den Seitenzuweiser mheap.pages markiert, dass der angegebene Seitenspeicher freigegeben wurde. Dann wird der Status von mspan auf mSpanDead gesetzt. Schließlich wird mspan durch den Zuweiser mheap.spanalloc freigegeben:
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))
}Wenn der P-Cache noch nicht voll ist, wird er in den lokalen P-Cache für die spätere Verwendung gestellt. Andernfalls wird er an den Heap-Speicher freigegeben.
Erweiterung
Der von heaparena verwaltete Seitenspeicherplatz wird nicht bereits zu Beginn vollständig angefordert. Speicher wird erst zugewiesen, wenn er benötigt wird. Verantwortlich für die Erweiterung des Heap-Speichers ist die Methode mheap.grow. Hier ist der vereinfachte Code:
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)
}
...
}Zuerst wird basierend auf npage der benötigte Speicher berechnet und ausgerichtet. Dann wird beurteilt, ob das aktuelle heaparena genügend Speicher hat. Wenn nicht, wird durch mheap.sysAlloc mehr Speicher für das aktuelle heaparena angefordert oder ein neues heaparena zugewiesen:
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
}
}
...
}Zuerst wird versucht, den linearen Zuweiser mheap.arena zu verwenden, um einen Speicherblock aus dem vorab zugewiesenen Speicherplatz anzufordern. Wenn dies fehlschlägt, wird basierend auf hintList erweitert. Der Typ von hintList ist runtime.arenaHint, das speziell Adressinformationen für die heaparena-Erweiterung aufzeichnet:
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))
}Nach Abschluss der Speicheranforderung wird er in das zweidimensionale arenas-Array aktualisiert:
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))
}Schließlich markiert der Seitenzuweiser diesen Speicher als bereit:
// Update the page allocator's structures to make this
// space ready for allocation.
h.pages.grow(v, nBase-v)
totalGrowth += nBase - vObjektzuweisung
Wenn Go Speicher für Objekte zuweist, werden je nach Größe drei verschiedene Typen unterschieden:
- Mikroobjekte - tiny, kleiner als 16B
- kleine Objekte - small, kleiner als 32KB
- große Objekte - large, größer als 32KB
Je nach den drei verschiedenen Typen wird bei der Speicherzuweisung eine andere Logik ausgeführt. Verantwortlich für die Speicherzuweisung von Objekten ist die Funktion runtime.mallocgc. Ihre Funktionssignatur lautet:
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.PointerSie hat nur drei Parameter: Speichergröße, Typ und einen booleschen Wert, der angibt, ob der Speicher gelöscht werden soll. Sie ist die Eintrittsfunktion für alle Go-Objekt-Speicherzuweisungen. Auch bei der Verwendung der new-Funktion zum Erstellen von Zeigern wird diese Funktion aufgerufen. Nach erfolgreicher Speicherzuweisung ist der zurückgegebene Zeiger die Adresse des Objekts. Wie im mspan-Abschnitt erwähnt, besitzt jeder mspan ein spanClass. spanClass bestimmt die feste Größe des mspan. Go unterteilt Objekte im Bereich [0, 32KB] in 68 verschiedene Größen. Daher besteht der Go-Speicher aus mehreren mspan-Listen unterschiedlicher fester Größen. Bei der Zuweisung von Objektspeicher muss nur basierend auf der Objektgröße der entsprechende spanClass berechnet werden. Dann wird anhand von spanClass die entsprechende mspan-Liste gefunden und schließlich ein verfügbarer mspan aus der Liste gesucht. Diese abgestufte Vorgehensweise kann das Problem der Speicherfragmentierung relativ effektiv lösen.

Mikroobjekte
Alle Mikroobjekte ohne Zeiger, die kleiner als 16B sind, werden durch den Mikrozuweiser in P in einem zusammenhängenden Speicherbereich zugewiesen. In runtime.mcache zeichnet das Feld tiny die Basisadresse dieses Speicherbereichs auf:
type mcache struct {
tiny uintptr
tinyoffset uintptr
tinyAllocs uintptr
}Die Größe von Mikroobjekten wird durch die Konstante runtime.maxTinySize bestimmt, die 16B beträgt. Die Größe der Speicherblöcke zum Speichern von Mikroobjekten ist ebenfalls dieser Wert. Im Allgemeinen werden hier kleine Zeichenketten gespeichert. Der für die Zuweisung von Mikroobjekten verantwortliche Code sieht wie folgt aus:
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 = maxTinySizeWenn der aktuelle Mikrospeicherblock noch genügend Platz hat, wird der aktuelle Speicherblock direkt verwendet, d.h. off+size <= maxTinySize. Wenn nicht genug Platz vorhanden ist, wird zuerst versucht, einen verfügbaren Platz aus dem mspan-Cache von mcache zu finden. Wenn dies auch nicht möglich ist, wird ein mspan von mcentral angefordert. Wie auch immer, am Ende wird eine verfügbare Adresse erhalten. Dann wird der alte Mikroobjekt-Speicherblock durch den neuen ersetzt.
Kleine Objekte
Die meisten Objekte in der Go-Laufzeitumgebung sind kleine Objekte im Bereich [16B, 32KB]. Der Zuweisungsprozess für kleine Objekte ist am kompliziertesten, aber der Code ist am wenigsten. Der für die Zuweisung kleiner Objekte verantwortliche Code sieht wie folgt aus:
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)
}Zuerst wird basierend auf der Objektgröße berechnet, welche Art von spanClass verwendet werden soll. Dann wird durch runtime.nextFreeFast basierend auf spanClass versucht, verfügbaren Speicherplatz aus dem entsprechenden mspan-Cache von mcache zu holen:
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
}Die Funktion von mspan.allocCache besteht darin, aufzuzeichnen, ob Speicherplatz von Objekten verwendet wird. Es teilt den Speicher nicht nach Platzgröße auf, sondern nach Objektanzahl. Dies entspricht der Betrachtung von mspan als Objektarray, wie in der folgenden Abbildung gezeigt.

allocCache ist eine 64-Bit-Zahl, wobei jedes Bit einem Speicherplatz entspricht. Wenn ein Bit 0 ist, bedeutet dies, dass ein Objekt verwendet wird. Wenn es 1 ist, bedeutet dies, dass dieser Speicherplatz frei ist. sys.TrailingZeros64(s.allocCache) berechnet die Anzahl der nachfolgenden Nullen. Wenn das Ergebnis 64 ist, bedeutet dies, dass kein freier Speicher verfügbar ist. Wenn freier Speicher vorhanden ist, wird der Offset des freien Speichers berechnet, die Basisadresse des mspan hinzugefügt und zurückgegeben.
Wenn mcache nicht genügend Platz hat, wird mcentral angefragt. Diese Arbeit wird von der Methode mcache.nextFree erledigt:
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
}Die Methode mcache.refill ist dafür verantwortlich, einen verfügbaren mspan von mcentral anzufordern:
func (c *mcache) refill(spc spanClass) {
...
s = mheap_.central[spc].mcentral.cacheSpan()
...
}Die Methode mcentral.cacheSpan erweitert durch mcentral.grow, wenn nicht genügend Speicher vorhanden ist. Bei der Erweiterung wird wiederum ein neuer mspan von mheap angefordert:
func (c *mcentral) grow() *mspan {
...
s := mheap_.alloc(npages, c.spanclass)
...
return s
}Zusammenfassend erfolgt die Speicherzuweisung für kleine Objekte stufenweise: zuerst mcache, dann mcentral und schließlich mheap. Die Zuweisung durch mcache ist am kostengünstigsten, da es sich um den lokalen Cache von P handelt und beim Zuweisen von Speicher keine Sperre gehalten werden muss. mcentral folgt als Nächstes. Das direkte Anfordern von Speicher von mheap ist am teuersten, da die Methode mheap.alloc um die globale Sperre des gesamten Heaps konkurriert.
Große Objekte
Die Zuweisung großer Objekte ist am einfachsten. Wenn die Objektgröße 32KB überschreitet, wird direkt ein neuer mspan von mheap angefordert, um das Objekt aufzunehmen. Der für die Zuweisung großer Objekte verantwortliche Code sieht wie folgt aus:
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)
}
}Die Methode mcache.allocLarge ist dafür verantwortlich, Speicherplatz für große Objekte von mheap anzufordern:
func (c *mcache) allocLarge(size uintptr, noscan bool) *mspan {
...
spc := makeSpanClass(0, noscan)
s := mheap_.alloc(npages, spc)
...
return s
}Aus dem Code ist ersichtlich, dass für große Objekte der spanClass-Wert 0 verwendet wird. Grundsätzlich belegt ein großes Objekt einen mspan.
Weiteres
Speicherstatistik
Die Go-Laufzeitumgebung stellt dem Benutzer eine Funktion ReadMemStats zur Verfügung, die zur Statistik der Speichersituation zur Laufzeit verwendet werden kann:
func ReadMemStats(m *MemStats) {
_ = m.Alloc // nil check test before we switch stacks, see issue 61158
stopTheWorld(stwReadMemStats)
systemstack(func() {
readmemstats_m(m)
})
startTheWorld()
}Die Verwendung ist jedoch sehr kostspielig. Aus dem Code ist ersichtlich, dass vor der Analyse der Speichersituation STW erforderlich ist. Die STW-Dauer kann einige Millisekunden bis mehrere hundert Millisekunden betragen. Im Allgemeinen wird sie nur beim Debuggen und bei der Fehlersuche verwendet. Die Struktur runtime.MemStats zeichnet Informationen über Heap-Speicher, Stack-Speicher und GC auf:
type MemStats struct {
// Gesamtstatistik
Alloc uint64
TotalAlloc uint64
Sys uint64
Lookups uint64
Mallocs uint64
Frees uint64
// Heap-Speicher-Statistik
HeapAlloc uint64
HeapSys uint64
HeapIdle uint64
HeapInuse uint64
HeapReleased uint64
HeapObjects uint64
// Stack-Speicher-Statistik
StackInuse uint64
StackSys uint64
// Speicherkomponenten-Statistik
MSpanInuse uint64
MSpanSys uint64
MCacheInuse uint64
MCacheSys uint64
BuckHashSys uint64
// GC-bezogene Statistik
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
Der Speicherzuweiser wird offensichtlich für die Zuweisung von Heap-Speicher verwendet. Der Heap ist jedoch in zwei Teile unterteilt: Ein Teil ist der Heap-Speicher, den die Go-Laufzeitumgebung selbst benötigt, und der andere Teil ist der Heap-Speicher, der Benutzern zur Verfügung steht. Daher können in einigen Strukturen solche eingebetteten Felder gesehen werden:
_ sys.NotInHeapDies bedeutet, dass der Speicher dieses Typs nicht auf dem Benutzer-Heap zugewiesen wird. Solche eingebetteten Felder sind in Speicherzuweisungskomponenten besonders häufig, wie zum Beispiel in der Struktur runtime.mheap, die den Benutzer-Heap repräsentiert:
type mheap struct {
_ sys.NotInHeap
}Die eigentliche Funktion von sys.NotInHeap besteht darin, Speicherbarrieren zu vermeiden, um die Laufzeiteffizienz zu erhöhen. Der Benutzer-Heap muss GC ausführen, daher sind Speicherbarrieren erforderlich.
