Memory Allocator Deep Dive

Updated

September 8, 2026

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

  • Each P owns an mcache so allocation does not bounce on a global lock.
  • GC sweeps spans; scavenger returns idle pages to the OS (239).
  • Allocation pressure drives the pacer (208).

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=. -benchmem
func 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 byte

What 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).