memory
Diferentemente do C/C++ tradicional, Go é uma linguagem com GC, onde na maioria dos casos a alocação e liberação de memória é gerenciada automaticamente pelo Go. O compilador decide se a memória de um objeto deve ser alocada na stack ou no heap, basicamente sem necessidade de intervenção do usuário. O usuário apenas usa a memória. Em Go, o gerenciamento de memória do heap tem dois componentes principais: o alocador de memória负责 alocação de memória do heap, e o coletor de lixo负责回收 e liberação de memória de heap não utilizada. Este artigo foca principalmente no funcionamento do alocador de memória. O alocador de memória do Go é fortemente influenciado pelo TCMalloc (Thread-Caching Malloc) do Google.
Alocador
Em Go, existem dois tipos de alocadores de memória: alocador linear e alocador em cadeia.
Alocação Linear
O alocador linear corresponde à estrutura runtime.linearAlloc, conforme mostrado abaixo:
type linearAlloc struct {
next uintptr // próximo byte disponível
mapped uintptr // um byte após o fim do espaço mapeado
end uintptr // fim do espaço reservado
mapMemory bool // transiciona memória de Reserved para Ready se true
}Este alocador solicita antecipadamente do sistema operacional um espaço de memória contíguo. next aponta para o endereço de memória disponível, end aponta para o endereço final do espaço de memória, que pode ser entendido conforme o diagrama abaixo:

O método de alocação de memória do alocador linear é muito fácil de entender: verifica-se se há espaço restante suficiente para acomodar de acordo com o tamanho da memória solicitada. Se houver espaço suficiente, atualiza-se o campo next e retorna-se o endereço inicial do espaço restante. O código é conforme abaixo:
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)
}A vantagem deste método de alocação é ser rápido e simples. A desvantagem também é bastante óbvia: não é possível reutilizar memória já liberada, pois o campo next só aponta para o endereço de memória do espaço restante, não sendo possível perceber o espaço de memória já utilizado e depois liberado. Isso causa grande desperdício de espaço de memória, conforme mostrado no diagrama abaixo:

Portanto, alocação linear não é o principal método de alocação em Go. Ela é usada apenas como funcionalidade de pré-alocação de memória em máquinas de 32 bits.
Alocação em Cadeia
O alocador em cadeia corresponde à estrutura runtime.fixalloc. A memória alocada pelo alocador em cadeia não é contígua, existindo na forma de lista encadeada unidirecional. O alocador em cadeia é composto por vários blocos de memória de tamanho fixo, e cada bloco de memória é composto por vários fragmentos de memória de tamanho fixo. Cada vez que é realizada alocação de memória, é usado um fragmento de memória de tamanho fixo.
type fixalloc struct {
size uintptr
first func(arg, p unsafe.Pointer) // chamado na primeira vez que p é retornado
arg unsafe.Pointer
list *mlink
chunk uintptr // usa uintptr ao invés de unsafe.Pointer para evitar barreiras de escrita
nchunk uint32 // bytes restantes no chunk atual
nalloc uint32 // tamanho de novos chunks em bytes
inuse uintptr // bytes em uso atualmente
stat *sysMemStat
zero bool // zerar alocações
}
type mlink struct {
_ sys.NotInHeap
next *mlink
}Seus campos não são tão simples e fáceis de entender quanto o alocador linear. Aqui apresentamos brevemente os importantes:
size: refere-se a quanto memória é usada cada vez que é realizada alocação de memórialist: aponta para o nó cabeça da lista de fragmentos de memória reutilizáveis; o tamanho de cada fragmento de memória é determinado porsizechunk: aponta para o endereço ocioso no bloco de memória atualmente em usonchunk: número de bytes disponíveis restantes no bloco de memória atualnalloc: tamanho do bloco de memória, fixo em 16KBinuse: total de bytes de memória já utilizadoszero: ao reutilizar bloco de memória, se a memória deve ser zerada
O alocador em cadeia mantém referência ao bloco de memória atual e aos fragmentos de memória reutilizáveis. O tamanho de cada bloco de memória é fixo em 16KB, valor que é definido durante inicialização.
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
}A distribuição dos blocos de memória é conforme mostrado no diagrama abaixo. Os blocos de memória no diagrama estão organizados por ordem de tempo de criação. Na realidade, seus endereços não são contíguos.

O tamanho de memória alocado cada vez pelo alocador em cadeia também é fixo, determinado por fixalloc.size. Durante alocação, primeiro verifica-se se há bloco de memória reutilizável. Se houver, prioriza-se o uso do bloco de memória reutilizável. Caso contrário, usa-se o bloco de memória atual. Se o espaço restante do bloco de memória atual não for suficiente para acomodar, cria-se um novo bloco de memória. Esta parte da lógica corresponde ao código abaixo:
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
}A vantagem do alocador em cadeia é justamente poder reutilizar memória liberada. A unidade básica para reutilizar memória é um fragmento de memória de tamanho fixo, cujo tamanho é determinado por fixalloc.size. Ao liberar memória, o alocador em cadeia adiciona este fragmento de memória como nó cabeça à lista de fragmentos de memória ociosos. O código é conforme mostrado abaixo:
func (f *fixalloc) free(p unsafe.Pointer) {
f.inuse -= f.size
v := (*mlink)(p)
v.next = f.list
f.list = v
}Componentes de Memória
O alocador de memória em Go é composto principalmente pelos componentes mspan, heaparena, mcache, mcentral, mheap. Eles atuam em camadas, gerenciando toda a memória heap do Go.
mspan

runtime.mspan é a unidade básica na alocação de memória do Go. Sua estrutura é conforme abaixo:
type mspan struct {
next *mspan // próximo span na lista, ou nil se nenhum
prev *mspan // span anterior na lista, ou nil se nenhum
startAddr uintptr // endereço do primeiro byte do span, também conhecido como s.base()
npages uintptr // número de páginas no span
freeindex uintptr
spanclass spanClass // classe de tamanho e noscan (uint8)
needzero uint8 // precisa ser zerado antes da alocação
elemsize uintptr // computado a partir de sizeclass ou de npages
limit uintptr // fim dos dados no span
state mSpanStateBox // mSpanInUse etc; acessado atomicamente (métodos get/set)
nelems uintptr // número de objetos no span
allocCache uint64
allocCount uint16 // número de objetos alocados
...
}mspan e mspan são linkados na forma de lista encadeada bidirecional através de next e prev. Os endereços de memória não são contíguos. Cada mspan gerencia mspan.npages páginas de memória de tamanho runtime.pageSize. Normalmente, o tamanho da página é 8KB. mspan.startAddr registra o endereço inicial destas páginas, e mspan.limit registra o endereço final da memória já utilizada. O tamanho dos elementos armazenados em cada mspan (elemsize) é fixo, então a quantidade de elementos que pode acomodar também é fixa. Devido à quantidade fixa, o armazenamento de objetos é como um array distribuído em mspan, no intervalo [0, nelems]. freeindex registra o próximo índice disponível para armazenar objetos. mspan tem três estados no total:
mSpanDead: memória já foi liberadamSpanInUse: alocado no heapmSpanManual: alocado para parte de gerenciamento manual de memória, como stack
O que determina o tamanho dos elementos de mspan é spanClass. spanClass é um número inteiro do tipo uint8. Os sete bits mais altos armazenam o valor class indicando 0-67. O último bit é usado para indicar noscan, ou seja, se contém ponteiros.
type spanClass uint8
func (sc spanClass) sizeclass() int8 {
return int8(sc >> 1)
}
func (sc spanClass) noscan() bool {
return sc&1 != 0
}Ele tem um total de 68 valores diferentes. Todos os valores são armazenados no arquivo runtime.sizeclasses.go na forma de tabela. Em tempo de execução, usa-se spanClass através de runtime.class_to_size para obter o tamanho do objeto de mspan, e através de class_to_allocnpages para obter o número de páginas de mspan.
| class | tamanho máximo do objeto | tamanho do span | quantidade de objetos | desperdício no final | taxa máxima de desperdício de memória | alinhamento mínimo |
|---|---|---|---|---|---|---|
| 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 |
A lógica de cálculo destes valores pode ser encontrada na função printComment em runtime.mksizeclasses.go. A fórmula de cálculo da taxa máxima de desperdício de memória é:
float64((size-prevSize-1)*objects+tailWaste) / float64(spanSize)Por exemplo, quando class é 2, sua taxa máxima de desperdício de memória é:
((16-8-1)*512+0)/8192 = 0.4375Quando o valor de class é 0, é o spanClass usado especificamente para alocar objetos grandes acima de 32KB. Basicamente, um objeto grande ocupa um mspan. Portanto, o heap do Go é na verdade composto por várias listas encadeadas mspan de tamanhos fixos diferentes.
heaparena

Como mencionado anteriormente, mspan é composto por várias páginas, mas mspan apenas mantém referência aos endereços das páginas, não负责 gerenciar estas páginas. Quem realmente负责 gerenciar estas páginas de memória é runtime.heaparena. Cada heaparena gerencia várias páginas. O tamanho de heaparena é determinado por runtime.heapArenaBytes, normalmente 64MB. bitmap é usado para identificar se o endereço correspondente na página contém objetos. zeroedBase é o endereço inicial da página de memória gerenciada por este heaparena. spans registra qual mspan está usando cada página.
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
}A lógica relacionada ao registro de páginas e mspan pode ser encontrada no método mheap.setSpans, conforme mostrado abaixo:
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
}
}No heap do Go, é um array bidimensional de heaparena que gerencia todas as páginas de memória. Veja o campo mheap.arenas:
type mheap struct {
arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
}Na plataforma Windows de 64 bits, a primeira dimensão do array é 1 << 6, a segunda dimensão é 1 << 16. Na plataforma Linux de 64 bits, a primeira dimensão é 1, a segunda dimensão é 1 << 22. Este array bidimensional composto por todos os heaparena constitui o espaço de memória virtual em tempo de execução do Go. A visão geral é conforme mostrado no diagrama abaixo:

Embora os heaparena sejam adjacentes, as páginas de memória que eles gerenciam não são contíguas entre si.
mcache
mcache corresponde à estrutura runtime.mcache, que já apareceu no artigo de escalonamento concorrente. Embora seu nome seja mcache, na verdade está vinculado ao processador P. mcache é o cache de memória de cada processador P, que inclui o array de lista encadeada mspan alloc. O tamanho do array é fixo em 136, exatamente o dobro da quantidade de spanClass. Também inclui o cache de micro-objetos tiny, onde tiny aponta para o endereço inicial da memória de micro-objetos, tinyoffset é o offset da memória ociosa em relação ao endereço inicial, e tinyAllocs indica quantos micro-objetos foram alocados. Sobre o cache de stack stackcache, pode-se consultar Alocação de Memória de Stack.
type mcache struct {
_ sys.NotInHeap
nextSample uintptr // aciona amostra de heap após alocar esta quantidade de bytes
scanAlloc uintptr // bytes de heap escaneável alocados
tiny uintptr
tinyoffset uintptr
tinyAllocs uintptr
alloc [numSpanClasses]*mspan
stackcache [_NumStackOrders]stackfreelist
flushGen atomic.Uint32
}Durante inicialização, as listas encadeadas em alloc de mcache contêm apenas um nó cabeça vazio runtime.emptymspan, ou seja, mspan sem memória disponível.
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
}Apenas quando é necessário realizar alocação de memória, solicita-se um novo mspan de mcentral para substituir o span vazio original. Este trabalho é completado pelo método mcache.refill. Sua única entrada de chamada é a função runtime.mallocgc. Abaixo está o código simplificado:
func (c *mcache) refill(spc spanClass) {
// Retorna o span em cache atual para as listas centrais
s := c.alloc[spc]
// Obtém um novo span em cache das listas centrais
s = mheap_.central[spc].mcentral.cacheSpan()
if s == nil {
throw("out of memory")
}
c.scanAlloc = 0
c.alloc[spc] = s
}A vantagem de usar mcache é que não é necessário lock global durante alocação de memória. Porém, quando sua memória é insuficiente, é necessário acessar mcentral, o que ainda requer lock.
mcentral
runtime.mcentral gerencia todos os mspan no heap que armazenam objetos pequenos. Ao solicitar memória em mcache, é mcentral que realiza a alocação.
type mcentral struct {
_ sys.NotInHeap
spanclass spanClass
partial [2]spanSet
full [2]spanSet
}Os campos de mcentral são poucos: spanClass indica o tipo de mspan armazenado; partial e full são dois spanSet, o primeiro armazena mspan com memória ociosa, o segundo armazena mspan sem memória ociosa. mcentral é gerenciado diretamente pelo heap mheap. Em tempo de execução, existem um total de 136 mcentral.
type mheap struct {
central [numSpanClasses]struct {
mcentral mcentral
pad [(cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize) % cpu.CacheLinePadSize]byte
}
}mcentral é principalmente responsável por dois trabalhos: quando há memória suficiente, aloca mspan disponível para mcache; quando a memória é insuficiente, solicita de mheap a alocação de um novo mspan. O trabalho de alocação de mspan para mcache é completado pelo método mcentral.cacheSpan. Primeiro, busca-se mspan disponível no conjunto varrido da lista ociosa:
// Tenta spans parciais varridos primeiro
sg := mheap_.sweepgen
if s = c.partialSwept(sg).pop(); s != nil {
goto havespan
}Se não encontrar, busca-se mspan disponível no conjunto não varrido da lista ociosa:
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
}
}Se ainda não encontrar, busca-se no conjunto não varrido da lista não ociosa:
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)
}
}Se finalmente ainda não encontrar, o método mcentral.grow solicita de mheap a alocação de um novo mspan:
s = c.grow()
if s == nil {
return nil
}Em condições normais, de qualquer forma será retornado um mspan disponível:
havespan:
freeByteBase := s.freeindex &^ (64 - 1)
whichByte := freeByteBase / 8
// Inicializa cache de bits de alocação
s.refillAllocCache(whichByte)
s.allocCache >>= s.freeindex % 64
return sPara o processo de solicitar mspan de mheap, na verdade é chamada o método mheap.alloc, que retorna um novo mspan:
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
}Após inicializá-lo adequadamente, pode ser alocado para mcache usar.
mheap

runtime.mheap é o gerenciador da memória heap da linguagem Go. Em tempo de execução, existe como variável global runtime.mheap_:
var mheap_ mheapEle gerencia todos os mspan criados, todos os mcentral, todos os heaparena, e muitos outros alocadores variados. Sua estrutura simplificada é conforme abaixo:
type mheap struct {
_ sys.NotInHeap
lock mutex
allspans []*mspan // todos os spans lá fora
pagesInUse atomic.Uintptr // páginas de spans em stats mSpanInUse
pagesSwept atomic.Uint64 // páginas varridas neste ciclo
pagesSweptBasis atomic.Uint64 // pagesSwept para usar como origem da razão de varredura
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 // estrutura de dados de alocação de páginas
spanalloc fixalloc // alocador para span*
cachealloc fixalloc // alocador para mcache*
specialfinalizeralloc fixalloc // alocador para specialfinalizer*
specialprofilealloc fixalloc // alocador para specialprofile*
specialReachableAlloc fixalloc // alocador para specialReachable
specialPinCounterAlloc fixalloc // alocador para specialPinCounter
arenaHintAlloc fixalloc // alocador para arenaHints
}Para mheap, em tempo de execução há principalmente quatro trabalhos a serem realizados:
- Inicializar heap
- Alocar
mspan - Liberar
mspan - Expansão de heap
Abaixo vamos abordar estas quatro coisas em ordem.
Inicialização
O período de inicialização do heap está localizado na fase de bootstrap do programa, que também é a fase de inicialização do escalonador. A ordem de chamada é:
schedinit() -> mallocinit() -> mheap_.init()Durante o período de inicialização, é principalmente responsável por executar o trabalho de inicialização de cada alocador:
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)
}Isso inclui o alocador mheap.spanalloc负责分配 mspan e o alocador mheap.pages负责分配 páginas, bem como a inicialização de todos os mcentral.
Alocação
Em mheap, a alocação de mspan é completada pelo método mheap.allocSpan:
func (h *mheap) allocSpan(npages uintptr, typ spanAllocType, spanclass spanClass) (s *mspan)Se a memória solicitada for suficientemente pequena, ou seja, satisfaz npages < pageCachePages/4, tenta-se sem lock obter um mspan disponível no cache mspan do P local. Se o cache do P estiver vazio, também será inicializado primeiro:
// Se o cache estiver vazio, reabasteça-o
if c.empty() {
lock(&h.lock)
*c = h.pages.allocToCache()
unlock(&h.lock)
}Depois obtém-se do cache do P, completado pelo método mheap.tryAllocMSpan:
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
}
}
}O código para obter mspan do cache do P é conforme abaixo. Ele tenta obter o último mspan no cache:
func (h *mheap) tryAllocMSpan() *mspan {
pp := getg().m.p.ptr()
// Se não temos um p ou o cache está vazio, não podemos fazer
// nada aqui
if pp == nil || pp.mspancache.len == 0 {
return nil
}
// Retira a última entrada no cache
s := pp.mspancache.buf[pp.mspancache.len-1]
pp.mspancache.len--
return s
}Se a memória solicitada for relativamente grande, será alocada no heap. Durante este processo é necessário manter lock:
lock(&h.lock)
if base == 0 {
// Tenta adquirir um endereço base
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 {
// Falhamos em obter um mspan anteriormente, então pegamos
// um agora que temos o lock do heap
s = h.allocMSpanLocked()
}
unlock(&h.lock)Primeiro usa-se pageAlloc.alloc para alocar memória de página suficiente. Se a memória do heap não for suficiente, mheap.grow realiza expansão. Após conclusão da alocação de memória de página, aloca-se 64 mspan do alocador em cadeia mheap.spanalloc para o cache local do P (64 é exatamente metade do comprimento do array de cache). Depois retorna-se um mspan disponível do cache do P:
func (h *mheap) allocMSpanLocked() *mspan {
assertLockHeld(&h.lock)
pp := getg().m.p.ptr()
if pp == nil {
// Não temos um p então apenas fazemos o normal
return (*mspan)(h.spanalloc.alloc())
}
// Reabastece o cache se necessário
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
}
// Retira a última entrada no cache
s := pp.mspancache.buf[pp.mspancache.len-1]
pp.mspancache.len--
return s
}De acordo com as duas situações acima, finalmente pode-se obter um mspan disponível. Finalmente, após inicializar mspan adequadamente, pode-se retornar:
HaveSpan:
h.initSpan(s, typ, spanclass, base, npages)
return sLiberação
Como mspan é alocado pelo alocador em cadeia, naturalmente ao liberar memória também é ele que realiza a liberação:
func (h *mheap) freeSpanLocked(s *mspan, typ spanAllocType) {
assertLockHeld(&h.lock)
// Marca o espaço como livre
h.pages.free(s.base(), s.npages)
s.state.set(mSpanDead)
h.freeMSpanLocked(s)
}Primeiro marca-se através do alocador de páginas mheap.pages que a página de memória especificada foi liberada. Depois define-se o estado de mspan como mSpanDead. Finalmente, o alocador mheap.spanalloc libera mspan:
func (h *mheap) freeMSpanLocked(s *mspan) {
assertLockHeld(&h.lock)
pp := getg().m.p.ptr()
// Primeiro tenta liberar o mspan diretamente para o cache
if pp != nil && pp.mspancache.len < len(pp.mspancache.buf) {
pp.mspancache.buf[pp.mspancache.len] = s
pp.mspancache.len++
return
}
// Falhando nisso (ou se não temos um p), apenas libera
// de volta para a memória heap
h.spanalloc.free(unsafe.Pointer(s))
}Se o cache do P não estiver cheio, será colocado no cache local do P para continuar uso. Caso contrário, será liberado de volta para a memória heap.
Expansão
O espaço de memória de página gerenciado por heaparena não é todo solicitado antecipadamente no início. Apenas quando é necessário usar memória é que é alocado. Quem负责 expansão de memória heap é o método mheap.grow. Abaixo está o código simplificado:
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 {
// Muda para o novo espaço
h.curArena.base = uintptr(av)
h.curArena.end = uintptr(av) + asize
}
nBase = alignUp(h.curArena.base+ask, physPageSize)
}
...
}Primeiro calcula-se a memória necessária de acordo com npage e realiza-se alinhamento. Depois julga-se se o heaparena atual tem memória suficiente. Se não tiver, mheap.sysAlloc solicita mais memória para o heaparena atual ou aloca um novo heaparena:
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
}
}
...
}Primeiro tenta-se usar o alocador linear mheap.arena para solicitar um bloco de memória no espaço de memória pré-alocado. Se falhar, realiza-se expansão de acordo com hintList. hintList é do tipo runtime.arenaHint, que registra especificamente informações de endereço relacionadas à expansão de heaparena:
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))
}Após solicitação de memória, atualiza-se para o array bidimensional arenas:
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))
}Finalmente, o alocador de páginas marca esta memória como estado pronto:
// Atualiza estruturas do alocador de páginas para tornar este
// espaço pronto para alocação
h.pages.grow(v, nBase-v)
totalGrowth += nBase - vAlocação de Objetos
Em Go, ao alocar memória para objetos, divide-se em três tipos diferentes de acordo com o tamanho:
- Micro-objetos (tiny): menor que 16B
- Pequenos objetos (small): menor que 32KB
- Grandes objetos (large): maior que 32KB
De acordo com os três tipos diferentes, executa-se lógica diferente durante alocação de memória. A função负责分配 memória para objetos é runtime.mallocgc. Sua assinatura de função é conforme abaixo:
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.PointerEla tem apenas três parâmetros: tamanho da memória, tipo, e um valor booleano用于表示 se é necessário limpar memória. É a função de entrada para toda alocação de memória de objetos em Go. Ao usar a função new para criar ponteiros, também se entra nesta função. Quando a alocação de memória é bem-sucedida, o ponteiro retornado é o endereço do objeto. Na parte mspan mencionamos que cada mspan possui um spanClass. spanClass determina o tamanho fixo de mspan. Go divide objetos no intervalo [0, 32KB] em 68 tamanhos diferentes. Portanto, a memória do Go é composta por várias listas encadeadas mspan de tamanhos fixos diferentes. Ao alocar memória de objeto, basta calcular o spanClass correspondente de acordo com o tamanho do objeto, depois encontrar a lista encadeada mspan correspondente de acordo com spanClass, e finalmente buscar mspan disponível na lista encadeada. Esta prática hierárquica pode resolver de forma mais eficaz o problema de fragmentação de memória.

Micro-objetos
Todos os micro-objetos não-ponteiro menores que 16B são alocados pelo micro-alocador em P para o mesmo bloco de memória contíguo. Em runtime.mcache, o campo tiny registra o endereço base deste bloco de memória:
type mcache struct {
tiny uintptr
tinyoffset uintptr
tinyAllocs uintptr
}O tamanho dos micro-objetos é determinado pela constante runtime.maxTinySize, que é 16B. O bloco de memória用于存储 micro-objetos também é deste tamanho. Geralmente, os objetos armazenados aqui são algumas pequenas strings. A parte do código负责分配 micro-objetos é conforme abaixo:
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
}
// Aloca um novo bloco maxTinySize
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 = maxTinySizeSe o bloco de micro-memória atual tiver espaço suficiente para acomodar, usa-se diretamente o bloco de memória atual, ou seja, off+size <= maxTinySize. Se não for suficiente, primeiro tenta-se buscar espaço disponível no cache span de mcache. Se também não for possível, solicita-se um mspan de mcentral. De qualquer forma, finalmente se obtém um endereço disponível. Por fim, substitui-se o bloco de memória de micro-objeto antigo pelo novo.
Pequenos Objetos
A maioria dos objetos em tempo de execução da linguagem Go são pequenos objetos no intervalo [16B, 32KB]. O processo de alocação de pequenos objetos é o mais complicado, mas o código é o menor. A parte do código负责分配 pequenos objetos é conforme abaixo:
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)
}Primeiro calcula-se qual spanClass deve ser usado de acordo com o tamanho do objeto. Depois, runtime.nextFreeFast tenta obter espaço de memória disponível no mspan em cache correspondente em mcache de acordo com spanClass:
func nextFreeFast(s *mspan) gclinkptr {
theBit := sys.TrailingZeros64(s.allocCache) // Há um objeto livre no 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
}A função de mspan.allocCache é registrar se há objetos usando espaço de memória. Ele divide memória um por um de acordo com a quantidade de objetos, não de acordo com o tamanho do espaço. Isso é equivalente a tratar mspan como um array de objetos, conforme mostrado no diagrama abaixo:

allocCache é um número de 64 bits. Cada bit corresponde a um bloco de espaço de memória. Se um determinado bit for 0, indica que há objeto usando. Se for 1, indica que este bloco de memória está ocioso. O objetivo de sys.TrailingZeros64(s.allocCache) é calcular a quantidade de zeros à direita. Se o resultado for 64, indica que não há memória ociosa disponível. Se houver, calcula-se o offset da memória ociosa e soma-se ao endereço base de mspan e retorna-se.
Quando mcache não tem espaço suficiente, solicita-se de mcentral. Este trabalho é completado pelo método mcache.nextFree:
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
}O mcache.refill nele负责 solicitar um mspan disponível de mcentral:
func (c *mcache) refill(spc spanClass) {
...
s = mheap_.central[spc].mcentral.cacheSpan()
...
}O método mcentral.cacheSpan, quando a memória é insuficiente, realiza expansão através de mcentral.grow. A expansão solicita novo mspan de mheap:
func (c *mcentral) grow() *mspan {
...
s := mheap_.alloc(npages, c.spanclass)
...
return s
}Portanto, finalmente, a alocação de memória de pequenos objetos é feita nível por nível: primeiro mcache, depois mcentral, e finalmente mheap. O custo de alocação de mcache é o mais baixo, pois é cache local do P, não requerendo lock durante alocação de memória. mcentral é o próximo. Solicitar memória diretamente de mheap tem o custo mais alto, pois o método mheap.alloc compete pelo lock global inteiro do heap.
Grandes Objetos
A alocação de grandes objetos é a mais simples. Se o tamanho do objeto exceder 32KB, solicita-se diretamente de mheap a alocação de um novo mspan para acomodar. A parte do código负责分配 grandes objetos é conforme abaixo:
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)
}
}Onde mcache.allocLarge负责 solicitar memória de grandes objetos de mheap:
func (c *mcache) allocLarge(size uintptr, noscan bool) *mspan {
...
spc := makeSpanClass(0, noscan)
s := mheap_.alloc(npages, spc)
...
return s
}Pode-se ver no código que o valor de spanClass usado por grandes objetos é 0. Grandes objetos basicamente ocupam um mspan por objeto.
Outros
Estatísticas de Memória
O runtime do Go expõe uma função ReadMemStats para usuários, que pode ser usada para estatísticas de memória do runtime:
func ReadMemStats(m *MemStats) {
_ = m.Alloc // teste de verificação de nil antes de trocarmos stacks, veja issue 61158
stopTheWorld(stwReadMemStats)
systemstack(func() {
readmemstats_m(m)
})
startTheWorld()
}Mas o custo de usá-la é muito alto. Pode-se ver no código que é necessário STW antes de analisar a situação da memória. A duração do STW pode variar de alguns milissegundos a centenas de milissegundos. Geralmente é usada apenas durante depuração e solução de problemas. A estrutura runtime.MemStats registra informações relacionadas à memória heap, memória stack e GC:
type MemStats struct {
// Estatísticas gerais
Alloc uint64
TotalAlloc uint64
Sys uint64
Lookups uint64
Mallocs uint64
Frees uint64
// Estatísticas de memória heap
HeapAlloc uint64
HeapSys uint64
HeapIdle uint64
HeapInuse uint64
HeapReleased uint64
HeapObjects uint64
// Estatísticas de memória stack
StackInuse uint64
StackSys uint64
// Estatísticas de componentes de memória
MSpanInuse uint64
MSpanSys uint64
MCacheInuse uint64
MCacheSys uint64
BuckHashSys uint64
// Estatísticas relacionadas ao gc
GCSys uint64
OtherSys uint64
NextGC uint64
LastGC uint64
PauseTotalNs uint64
PauseNs [256]uint64
PauseEnd [256]uint64
NumGC uint32
NumForcedGC uint32
GCCPUFraction float64
EnableGC bool
DebugGC bool
BySize [61]struct {
Size uint32
Mallocs uint64
Frees uint64
}
}NotInHeap
O alocador de memória é obviamente usado para alocar memória heap, mas o heap é dividido em duas partes: uma parte é a memória heap necessária para o próprio runtime do Go, e a outra parte é a memória heap disponível para usuários. Portanto, em algumas estruturas pode-se ver tal campo embutido:
_ sys.NotInHeapIndicando que a memória deste tipo não será alocada no heap do usuário. Este tipo de campo embutido é especialmente comum em componentes de alocação de memória, como a estrutura runtime.mheap que representa o heap do usuário:
type mheap struct {
_ sys.NotInHeap
}O verdadeiro propósito de sys.NotInHeap é evitar barreiras de memória para melhorar a eficiência do runtime. O heap do usuário precisa executar GC, então requer barreiras de memória.
