memory
従来の c/c++ とは異なり、go は gc 言語で、ほとんどの場合メモリの割り当てと破棄は go によって自動的に管理されます。オブジェクトのメモリがスタック上に割り当てられるかヒープ上に割り当てられるかはコンパイラによって決定され、基本的にユーザーはメモリ管理に参加する必要はなく、ユーザーが行うのはメモリを使用することだけです。go におけるヒープメモリ管理には主に 2 つの大きなコンポーネントがあります。メモリアロケータはヒープメモリの割り当てを担当し、ガベージコレクターは無用なヒープメモリを回収して解放する責任を負います。本記事では主にメモリアロケータの作業方式について説明します。go メモリアロケータはグーグルの TCMalloc メモリアロケータの影響を大きく受けています。
アロケータ
go には 2 種類のメモリアロケータがあります。1 つは線形アロケータで、もう 1 つはチェーンアロケータです。
線形割り当て
線形アロケータは runtime.linearAlloc 構造体に対応し、以下の通りです。
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
}このアロケータはオペレーティングシステムに連続したメモリスペースを事前に申請し、next は使用可能なメモリアドレスを指し、end はメモリスペースの末尾アドレスを指します。概ね以下の図のように理解できます。

線形アロケータのメモリ割り当て方式は非常に理解しやすく、申請するメモリサイズに基づいて十分な剩余スペースがあるかどうかをチェックします。十分な場合は next フィールドを更新して剩余スペースの開始アドレスを返します。コードは以下の通りです。
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)
}この割り当て方式の利点は高速でシンプルであることで、欠点も非常に明確です。解放されたメモリを再利用できないことです。next フィールドは剩余のスペースメモリアドレスのみを指し、以前使用されて解放されたメモリスペースを感知できないため、これにより大きなメモリスペースの浪費が発生します。以下の図の通りです。

したがって線形割り当ては go における主要な割り当て方式ではなく、32 ビットマシンでのみメモリ事前割り当て機能として使用されます。
チェーン割り当て
チェーンアロケータは構造体 runtime.fixalloc に対応し、チェーンアロケータによって割り当てられるメモリは連続しておらず、単方向リンクドリストの形式で存在します。チェーンアロケータは若干の固定サイズのメモリブロックで構成され、各メモリブロックは若干の固定サイズのメモリフラグメントで構成されます。メモリ割り当てを行うたびに、固定サイズのメモリフラグメントが使用されます。
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
}そのフィールドは線形アロケータほどシンプルで理解しやすいものではありません。ここでは重要なものを簡単に紹介します。
size、メモリ割り当て時に毎回使用するメモリ量を指します。list、再利用可能なメモリフラグメントのヘッダーノードを指し、各メモリスペースのサイズはsizeによって決定されます。chunk、現在使用されているメモリブロック内のアイドルアドレスを指しますnchunk、現在のメモリブロックの剩余利用可能バイト数nalloc、メモリブロックのサイズで、固定で 16KB です。inuse、总共に使用されたバイト数zero、メモリブロックを再利用する際にメモリをクリアするかどうか
チェーンアロケータは現在のメモリブロックと再利用可能なメモリフラグメントの参照を保持しています。各メモリブロックのサイズは固定で 16KB で、この値は初期化時に設定されます。
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
}メモリブロックの分布は以下の図の通りで、図中のメモリブロックは作成時間の先後によって並べられていますが、実際にはそれらのアドレスは連続していません。

チェーンアロケータによって毎回割り当てられるメモリサイズも固定で、fixalloc.size によって決定されます。割り当て時にはまず再利用可能なメモリブロックがあるかどうかをチェックし、ある場合は優先的に再利用メモリブロックを使用し、その後現在のメモリブロックを使用します。現在のメモリブロックの剩余スペースが容纳できない場合は新しいメモリブロックを作成します。この部分のロジックは以下のコードに対応します。
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
}チェーンアロケータの利点は解放されたメモリを再利用できることです。メモリを再利用する基本単位は固定サイズのメモリフラグメントで、そのサイズは fixalloc.size によって決定されます。メモリを解放する際、チェーンアロケータはそのメモリフラグメントをヘッダーノードとしてアイドルメモリフラグメントリンクドリストに追加します。コードは以下の通りです。
func (f *fixalloc) free(p unsafe.Pointer) {
f.inuse -= f.size
v := (*mlink)(p)
v.next = f.list
f.list = v
}メモリコンポーネント
go におけるメモリアロケータは主に mspan、heaparena、mcache、mcentral、mheap のいくつかのコンポーネントで構成され、それらは階層的に作用し、go のヒープメモリ全体を管理しています。
mspan

runtime.mspan は go メモリ割り当ての基本的な単位で、その構造は以下の通りです。
type mspan struct {
next *mspan // next span in list, or nil if none
prev *mspan // previous span in list, or nil if none
startAddr uintptr // address of first byte of span aka s.base()
npages uintptr // number of pages in span
freeindex uintptr
spanclass spanClass // size class and noscan (uint8)
needzero uint8 // needs to be zeroed before allocation
elemsize uintptr // computed from sizeclass or from npages
limit uintptr // end of data in span
state mSpanStateBox // mSpanInUse etc; accessed atomically (get/set methods)
nelems uintptr // number of object in the span.
allocCache uint64
allocCount uint16 // number of allocated objects
...
}mspan と mspan の間は next と prev によって双方向リンクドリストの形式でリンクされ、メモリアドレスは連続していません。各 mspan は mspan.npages 個の runtime.pageSize サイズのページメモリを管理し、通常ページサイズは 8KB で、mspan.startAddr がこれらのページの開始アドレスを記録し、mspan.limit が使用済みメモリの末端アドレスを記録します。各 mspan が格納する要素サイズ elemsize は固定であるため、容纳できる要素数も固定です。数が固定されているため、オブジェクトの格納は配列のように mspan 内に分布し、範囲は [0, nelems] で、freeindex が次にオブジェクトを格納するために使用できるインデックスを記録します。mspan には合計 3 つの状態があります。
- mSpanDead、メモリはすでに解放されています
- mSpanInUse、ヒープ上に割り当てられています
- mSpanManual、手動管理メモリ用に割り当てられています。例えばスタックなど。
mspan の要素サイズを決定するのは spanClass で、spanClass 自体は uint8 タイプの整数で、上位 7 ビットは 0-67 の class 値を格納し、最後のビットは noscan つまりポインタを含むかどうかを示すために使用されます。
type spanClass uint8
func (sc spanClass) sizeclass() int8 {
return int8(sc >> 1)
}
func (sc spanClass) noscan() bool {
return sc&1 != 0
}合計 68 種類の異なる値があり、すべての値はテーブル形式で runtime.sizeclasses.go ファイルに格納されています。実行時、spanClass を使用して runtime.class_to_size によって mspan のオブジェクトサイズを取得でき、class_to_allocnpages によって mspan のページ数を取得できます。
| class | 最大オブジェクトサイズ | span サイズ | オブジェクト数 | 末尾の浪費 | 最大メモリ浪費率 | 最小アライメント |
|---|---|---|---|---|---|---|
| 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 |
これらの値の計算ロジックは runtime.mksizeclasses.go の printComment 関数で見つけることができ、その最大メモリ浪費率の計算式は以下の通りです。
float64((size-prevSize-1)*objects+tailWaste) / float64(spanSize)例えば、class が 2 の場合、その最大メモリ浪費率は以下の通りです。
((16-8-1)*512+0)/8192 = 0.4375class 値が 0 の場合、32KB 以上の大オブジェクトを割り当てるために専用に使用される spanClass で、基本的に 1 つの大オブジェクトが 1 つの mspan を占有します。したがって、go のヒープメモリは実際には若干の異なる固定サイズの mspan で構成されています。
heaparena

前述したように mspan は若干のページで構成されていますが、mspan はページのアドレス参照を保持しているだけで、これらのページを管理する責任は負いません。実際にこれらのページメモリを管理する責任を負うのは runtime.heaparena です。各 heaparena は若干のページを管理し、heaparena のサイズは runtime.heapArenaBytes によって決定され、通常は 64MB です。bitmap はページ内の対応するアドレスがオブジェクトを格納しているかどうかを識別するために使用され、zeroedBase はその heaparena が管理するページメモリの開始アドレスで、spans によって各ページがどの mspan によって使用されているかが記録されます。
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
}ページと mspan 記録に関するロジックは mheap.setSpans メソッドで見つけることができ、以下の通りです。
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 ヒープでは、2 次元の heaparena 配列によってすべてのページメモリが管理されています。mheap.arenas フィールドを参照してください。
type mheap struct {
arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
}64 ビット Windows プラットフォームでは、配列の 1 次元は 1 << 6、2 次元は 1 << 16 です。64 ビット Linux プラットフォームでは、1 次元は 1、2 次元は 1 << 22 です。すべての heaparena で構成されるこの 2 次元配列は go ランタイムの仮想メモリスペースを構成し、全体的に見ると以下の図の通りです。

heaparena 間は隣接していますが、それらが管理するページメモリ間は連続していません。
mcache
mcache は runtime.mcache 構造体に対応し、並行スケジューリングの文書ですでに言及されています。名前は mcache ですが、実際にはプロセッサ P にバインドされています。mcache は各プロセッサ P 上のメモリキャッシュで、mspan リンクドリスト配列 alloc が含まれています。配列のサイズは固定で 136 で、ちょうど spanClass 数の 2 倍です。またマイクロオブジェクトキャッシュ tiny も含まれています。tiny はマイクロオブジェクトメモリの開始アドレスを指し、tinyoffset はアイドルメモリが開始アドレスに対して相対的なオフセットで、tinyAllocs は何個のマイクロオブジェクトが割り当てられたかを示します。スタックキャッシュ stackcache については、スタックメモリ割り当て で了解できます。
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
}初期化時、mcache 内の alloc 内のリンクドリストは空のヘッダーノード runtime.emptymspan のみを含んでいます。つまり利用可能なメモリのない mspan です。
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
}メモリ割り当てが必要な場合にのみ、元の空の span を置き換えるために mcentral から新しい mspan を申請します。この部分の作業は mcache.refill メソッドによって完了され、その唯一の呼び出し入口は runtime.mallocgc 関数です。以下は簡略化後のコードです。
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 を使用する利点は、メモリ割り当て時にグローバルロックが不要なことです。ただしメモリが不足した場合は mcentral にアクセスする必要があり、この時は依然としてロックが必要です。
mcentral
runtime.mcentral はヒープ内のすべての小オブジェクトを格納する mspan を管理し、mcache がメモリを申請する際も mcentral によって割り当てられます。
type mcentral struct {
_ sys.NotInHeap
spanclass spanClass
partial [2]spanSet
full [2]spanSet
}mcentral のフィールドは少なく、spanClass は格納される mspan タイプを示し、partial と full は 2 つの spanSet で、前者はアイドルメモリのある mspan を格納し、後者はアイドルメモリのない mspan を格納します。mcentral は mheap ヒープによって直接管理され、実行時には合計 136 個の mcentral があります。
type mheap struct {
central [numSpanClasses]struct {
mcentral mcentral
pad [(cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize) % cpu.CacheLinePadSize]byte
}
}mcentral は主に 2 つの作業を担当します。メモリが十分な場合に mcache に利用可能な mspan を割り当てることと、メモリが不足した場合に mheap に新しい mspan の割り当てを申請することです。mcache に mspan を割り当てる作業は mcentral.cacheSpan メソッドによって完了されます。まずアイドルリストの清扫済み集合から利用可能な mspan を探します。
// Try partial swept spans first.
sg := mheap_.sweepgen
if s = c.partialSwept(sg).pop(); s != nil {
goto havespan
}見つからない場合は、アイドルリストの未清掃集合から利用可能な mspan を探します。
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
}
}それでも見つからない場合は、非アイドルリストの未清掃集合を探します。
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)
}
}最終的にそれでも見つからない場合は、mcentral.grow メソッドによって mheap に新しい mspan の割り当てを申請します。
s = c.grow()
if s == nil {
return nil
}通常の状況では、どのようにしても利用可能な mspan が返されます。
havespan:
freeByteBase := s.freeindex &^ (64 - 1)
whichByte := freeByteBase / 8
// Init alloc bits cache.
s.refillAllocCache(whichByte)
s.allocCache >>= s.freeindex % 64
return smheap に mspan を申請するプロセスは、実際には mheap.alloc メソッドを呼び出すことで、このメソッドは新しい 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
}これを初期化した後、mcache に割り当てて使用できます。
mheap

runtime.mheap は go 言語ヒープメモリの管理者で、実行時にはグローバル変数 runtime.mheap_ として存在します。
var mheap_ mheapこれは作成されたすべての mspan、すべての mcentral、およびすべての heaparena、および多くの他のさまざまなアロケータを管理します。簡略化後の構造は以下の通りです。
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 にとって、実行時には主に以下の 4 つの作業を行う必要があります。
- ヒープの初期化
mspanの割り当てmspanの解放- ヒープの拡張
以下では順序に従ってこれら 4 つの事柄について説明します。
初期化
ヒープの初期化時期はプログラムのガイド段階にあり、同時にスケジューラの初期化段階でもあります。呼び出し順序は以下の通りです。
schedinit() -> mallocinit() -> mheap_.init()初期化時期には、主に各アロケータの初期化作業を実行する責任を負います。
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)
}これには mspan を割り当てる責任を負うアロケータ mheap.spanalloc とページ割り当てを担当するアロケータ mheap.pages、およびすべての mcentral の初期化が含まれます。
割り当て
mheap において、mspan の割り当てはすべて mheap.allocSpan メソッドによって完了されます。
func (h *mheap) allocSpan(npages uintptr, typ spanAllocType, spanclass spanClass) (s *mspan)申請された割り当てメモリが十分に小さい場合、つまり npages < pageCachePages/4 を満たす場合、ロックなしでローカル P 内の mspan キャッシュから利用可能な mspan を取得しようとします。P のキャッシュが空の場合は、まず初期化を行います。
// If the cache is empty, refill it.
if c.empty() {
lock(&h.lock)
*c = h.pages.allocToCache()
unlock(&h.lock)
}その後 P キャッシュから取得し、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
}
}
}P キャッシュから mspan を取得するコードは以下の通りで、キャッシュ内の最後の mspan を取得しようとします。
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
}申請されたメモリが比較的大きい場合は、ヒープ上でメモリを割り当てます。このプロセス中はロックを保持する必要があります。
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)まず pageAlloc.alloc を使用して十分なページメモリを割り当てます。ヒープメモリが不足している場合は mheap.grow によって拡張されます。ページメモリ割り当て完了後、チェーンアロケータ mheap.spanalloc によって 64 個の mspan が P ローカルキャッシュに割り当てられます。64 はちょうどキャッシュ配列長さの半分です。その後 P キャッシュから利用可能な mspan を返します。
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
}上記の 2 つの状況に基づき、最終的に利用可能な mspan を取得できます。最後に mspan を初期化完了後に返すことができます。
HaveSpan:
h.initSpan(s, typ, spanclass, base, npages)
return s解放
mspan がチェーンアロケータによって割り当てられているため、メモリを解放する際もそれによって解放されます。
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)
}まずページアロケータ mheap.pages によって指定されたページメモリが解放されたことをマークし、その後 mspan の状態を mSpanDead に設定し、最後に mheap.spanalloc アロケータによって mspan を解放します。
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))
}P キャッシュが満杯でない場合は、P ローカルキャッシュに配置して継続して使用し、そうでない場合はヒープメモリに解放されます。
拡張
heaparena が管理するページメモリスペースは初期にすべて申請されたわけではなく、メモリが必要になったときにのみ割り当てられます。ヒープメモリを拡張する責任を負うのは mheap.grow メソッドで、以下は簡略化後のコードです。
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)
}
...
}まず npage に基づいて必要なメモリを計算してアライメントを行い、その後現在の heaparena に十分なメモリがあるかどうかを判断します。不足している場合は mheap.sysAlloc によって現在の heaparena にさらにメモリを申請するか、新しい 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
}
}
...
}まず線形アロケータ mheap.arena を使用して事前割り当てされたメモリスペースからメモリを申請しようとします。失敗した場合は hintList に基づいて拡張を行います。hintList のタイプは runtime.arenaHint で、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))
}メモリ申請完了後、それを arenas 2 次元配列に更新します。
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))
}最後にページアロケータによってこのメモリスペースを準備完了状態にマークします。
// Update the page allocator's structures to make this
// space ready for allocation.
h.pages.grow(v, nBase-v)
totalGrowth += nBase - vオブジェクト割り当て
go がオブジェクトにメモリを割り当てる際、サイズに基づいて 3 つの異なるタイプに分類されます。
- マイクロオブジェクト - tiny、16B 未満
- 小オブジェクト - small、32KB 未満
- 大オブジェクト - large、32KB 超
3 つの異なるタイプに基づき、メモリを割り当てる際に異なるロジックが実行されます。オブジェクトにメモリを割り当てる責任を負う関数は runtime.mallocgc で、その関数シグネチャは以下の通りです。
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointerこれには 3 つのパラメータのみあります。メモリサイズ、タイプ、およびメモリをクリアする必要があるかどうかを示すブール値です。これはすべての go オブジェクトメモリ割り当ての入口関数で、通常 new 関数を使用してポインタを作成する際もこの関数に入ります。メモリ割り当て成功後、これが返すポインタはそのオブジェクトのアドレスになります。mspan 部分で言及したように、各 mspan は spanClass を持っており、spanClass は mspan の固定サイズを決定し、go はオブジェクトを [0, 32KB] の範囲で 68 種類の異なるサイズに分類しています。したがって go メモリは若干の異なる固定サイズの mspan リンクドリストで構成されています。オブジェクトメモリを割り当てる際、オブジェクトサイズに基づいて対応する spanClass を計算し、その後 spanClass に基づいて対応する mspan リンクドリストを見つけ、最後にリンクドリストから利用可能な mspan を探すだけです。この階層化された做法はメモリフラグメントの問題をより効果的に解決できます。

マイクロオブジェクト
16B 未満のすべての非ポインタマイクロオブジェクトは P 内のマイクロアロケータによって同じ連続メモリに割り当てられます。runtime.mcache 内では、tiny フィールドによってこのメモリのベースアドレスが記録されます。
type mcache struct {
tiny uintptr
tinyoffset uintptr
tinyAllocs uintptr
}マイクロオブジェクトのサイズは runtime.maxTinySize 定数によって決定され、すべて 16B で、マイクロオブジェクトを格納するために使用されるメモリブロックもこのサイズです。一般的にここに格納されるオブジェクトは一些小さな文字列などです。マイクロオブジェクトを割り当てる責任を負う部分のコードは以下の通りです。
if size <= maxSmallSize {
if noscan && size < maxTinySize {
off := c.tinyoffset
if off+size <= maxTinySize && c.tiny != 0 {
x = unsafe.Pointer(c.tiny + off)
c.tinyoffset = off + size
c.tinyAllocs++
mp.mallocing = 0
releasem(mp)
return x
}
// Allocate a new maxTinySize block.
span = c.alloc[tinySpanClass]
v := nextFreeFast(span)
if v == 0 {
v, span, shouldhelpgc = c.nextFree(tinySpanClass)
}
x = unsafe.Pointer(v)
(*[2]uint64)(x)[0] = 0
(*[2]uint64)(x)[1] = 0
if (size < c.tinyoffset || c.tiny == 0) {
c.tiny = uintptr(x)
c.tinyoffset = size
}
size = maxTinySize現在のマイクロメモリブロックに容纳するための十分なスペースがある場合、つまり off+size <= maxTinySize の場合は、直接現在のメモリブロックを使用します。十分でない場合は、まず mcache の span キャッシュから利用可能なスペースを探そうとし、それでもダメな場合は mcentral に mspan を申請します。どのようにしても最終的に利用可能なアドレスが取得でき、最後に新しいマイクロオブジェクトメモリブロックで古いものを置き換えます。
小オブジェクト
go 言語ランタイムの大部分のオブジェクトは [16B, 32KB] この範囲内の小オブジェクトです。小オブジェクトの割り当てプロセスが最も面倒ですが、コードは最も少ないです。小オブジェクト割り当てを担当する部分のコードは以下の通りです。
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)
}まずオブジェクトのサイズに基づいてどのクラスの spanClass を使用するべきかを計算し、その後 runtime.nextFreeFast によって spanClass に基づいて mcache 内の対応するキャッシュ mspan から利用可能なメモリスペースを取得しようとします。
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 の役割はメモリスペースにオブジェクトが使用されているかどうかを記録することで、サイズに基づいてメモリを划分するのではなくオブジェクト数に基づいてメモリを 1 つずつ划分します。これは mspan をオブジェクト配列と見なすようなもので、以下の図の通りです。

allocCache は 64 ビット数字で、各ビットは一片のメモリスペースに対応し、あるビットが 0 の場合はオブジェクトによって使用されていることを示し、1 の場合はこのメモリがアイドルであることを示します。sys.TrailingZeros64(s.allocCache) の目的は末尾ゼロの数を計算することで、結果が 64 の場合は使用可能なアイドルメモリがないことを示し、ある場合はアイドルメモリのオフセットを計算して mspan のベースアドレスに加えて返します。
mcache に十分なスペースがない場合は、mcentral に申請しに行きます。この部分の作業は 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
}その中の mcache.refill は mcentral に利用可能な mspan を申請する責任を負います。
func (c *mcache) refill(spc spanClass) {
...
s = mheap_.central[spc].mcentral.cacheSpan()
...
}mcentral.cacheSpan メソッドはメモリが不足した際に mcentral.grow によって拡張が行われます。拡張はさらに mheap に新しい mspan を申請します。
func (c *mcentral) grow() *mspan {
...
s := mheap_.alloc(npages, c.spanclass)
...
return s
}したがって最終的に見ると、小オブジェクトのメモリ割り当ては階層的に下に向かって進むことで、まずは mcache、然后是 mcentral、最后是 mheap。mcache 割り当てのコストは最も低く、それは P ローカルキャッシュであるため、メモリを割り当てる際にロックを保持する必要がありません。mcentral はそれより次で、直接 mheap にメモリを申請するコストが最も高く、mheap.alloc メソッドはヒープ全体のグローバルロックを競合するためです。
大オブジェクト
大オブジェクト割り当てが最もシンプルで、オブジェクトのサイズが 32KB を超えた場合は、直接 mheap に新しい mspan の割り当てを申請して容纳します。大オブジェクト割り当てを担当する部分のコードは以下の通りです。
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)
}
}その中の mcache.allocLarge は mheap に大オブジェクトのメモリスペースを申請する責任を負います。
func (c *mcache) allocLarge(size uintptr, noscan bool) *mspan {
...
spc := makeSpanClass(0, noscan)
s := mheap_.alloc(npages, spc)
...
return s
}コードから見ると、大オブジェクトが使用する spanClass 値は 0 で、大オブジェクトは基本的に 1 つのオブジェクトが 1 つの mspan を占有します。
その他
メモリ統計
go ランタイムはユーザーに 1 つの関数 ReadMemStats を公開しており、ランタイムのメモリ状況を統計するために使用できます。
func ReadMemStats(m *MemStats) {
_ = m.Alloc // nil check test before we switch stacks, see issue 61158
stopTheWorld(stwReadMemStats)
systemstack(func() {
readmemstats_m(m)
})
startTheWorld()
}しかしそれを使用する代償は非常に大きく、コードから見るとメモリ状況を分析する前に STW が必要で、STW の長さは数ミリ秒から数百ミリ秒まで異なる可能性があり、一般的にはデバッグと問題調査時にのみ使用されます。runtime.MemStats 構造体はヒープメモリ、スタックメモリ、および GC に関連する情報を記録します。
type MemStats struct {
// 总体统计
Alloc uint64
TotalAlloc uint64
Sys uint64
Lookups uint64
Mallocs uint64
Frees uint64
// 堆内存统计
HeapAlloc uint64
HeapSys uint64
HeapIdle uint64
HeapInuse uint64
HeapReleased uint64
HeapObjects uint64
// 栈内存统计
StackInuse uint64
StackSys uint64
// 内存组件统计
MSpanInuse uint64
MSpanSys uint64
MCacheInuse uint64
MCacheSys uint64
BuckHashSys uint64
// gc 相关的统计
GCSys uint64
OtherSys uint64
NextGC uint64
LastGC uint64
PauseTotalNs uint64
PauseNs [256]uint64
PauseEnd [256]uint64
NumGC uint32
NumForcedGC uint32
GCCPUFraction float64
EnableGC bool
DebugGC bool
BySize [61]struct {
Size uint32
Mallocs uint64
Frees uint64
}
}NotInHeap
メモリアロケータは明らかにヒープメモリを割り当てるために使用されますが、ヒープは 2 つの部分に分類されます。1 つは go ランタイム自体に必要なヒープメモリで、もう 1 つはユーザーに開放されるヒープメモリです。したがって一部の構造ではこのような埋め込みフィールドが見られます。
_ sys.NotInHeapこれはそのタイプのメモリがユーザーヒープ上に割り当てられないことを示し、この埋め込みフィールドはメモリ割り当てコンポーネントで特に一般的です。例えばユーザーヒープを表す構造体 runtime.mheap などです。
type mheap struct {
_ sys.NotInHeap
}sys.NotInHeap の実際の役割はメモリバリアを回避してランタイム効率を向上させることで、ユーザーヒープは GC を実行する必要があるためメモリバリアが必要です。
