Memory Allocator Deep Dive
Memory Allocator Deep Dive
Overview
Go’s allocator is a warehouse manager: hand out differently sized objects fast, keep fragmentation in check, and cooperate with the GC. Most small allocations hit a per-P cache and avoid locks.
Teaching map aligned with runtime walkthroughs such as Internals for Interns — Memory Allocator. Original prose for this book.
Diagram: three-level hierarchy
goroutine needs memory
│
v
┌───────────┐ hit return pointer
│ P.mcache │──────────────────────────►
│ (per-P) │
└─────┬─────┘
│ miss
v
┌───────────┐ hit
│ mcentral │──────────────────────────► refill mcache
│ size class│
└─────┬─────┘
│ miss
v
┌───────────┐
│ mheap │──► spans / arenas ──► OS (mmap)
└───────────┘
| Level | Role |
|---|---|
| mcache | Per-P free lists; lock-free for the running P |
| mcentral | Shared lists per size class when mcache empty |
| mheap | Global heap, spans, arenas; talks to OS |
Size classes and spans
tiny / small objects → fixed size classes → span of pages carved into slots
large objects → dedicated spans (page-rounded)
Go 1.27 generates size-specialized allocation calls for some objects under 80 bytes (up to ~30% cheaper on that path; ~1% overall on alloc-heavy programs). Disable with GOEXPERIMENT=nosizespecializedmalloc if you need a bisect.
flow:
[Class]
|
v
[Slot]
[Slot]
|
v
[Ptr]
Arenas and address space
Modern Go reserves large arenas of virtual address space, then commits pages as needed. That keeps metadata dense and growth predictable compared to many tiny mmaps.
Interaction with GC and P
Escape analysis still wins
The fastest allocation is no heap allocation. Stack frames and SSA prove many values need no allocator at all (215, 209).
Diagnosis
| Signal | Meaning |
|---|---|
runtime.mallocgc in CPU pprof |
alloc-heavy path |
| Heap profile top | retained objects |
| Alloc profile top | churn even if freed |
High GCCPUFraction |
pacer reacting to alloc rate |
Experiment
go test -bench=. -benchmemfunc BenchmarkHeap(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = make([]byte, 1024)
}
}
func BenchmarkStack(b *testing.B) {
for i := 0; i < b.N; i++ {
var a [1024]byte
sink = a[0]
}
}
var sink byteWhat to notice: stack-ish patterns show fewer alloc/op when the compiler keeps data off-heap.
Try next: GODEBUG=allocfreetrace=1 on a tiny program (noisy; educational only).