Memory Allocator — Specification¶
1. Status of this document¶
There is no formal specification for the Go memory allocator. The Go Programming Language Specification at https://go.dev/ref/spec defines the language; it does not define the runtime, the allocator, the garbage collector, or the scheduler. The Go 1 compatibility promise at https://go.dev/doc/go1compat applies to language semantics and public package APIs; it explicitly does not cover runtime implementation details. The source code in the runtime package, together with a constellation of design documents, proposals, and commit messages, IS the specification.
This document collects what is currently observable, documented, or proposed about Go's memory allocator: its origins, the runtime APIs that interact with allocation, the metrics it exposes, the GODEBUG knobs that change its behaviour, the MemStats reference, the authoritative source files, the design documents that shaped its evolution, and the reading order for new contributors. None of this is part of the Go 1 promise. Everything described here can change in any release; the file references and proposal numbers are stable, but the behaviours they describe are subject to the runtime team's discretion.
The allocator is a single component in a tightly coupled stack: it lives inside runtime, interacts continuously with the garbage collector (runtime/mgc.go, runtime/mgcmark.go, runtime/mgcsweep.go) and the scheduler (runtime/proc.go), and exposes its state through runtime.MemStats, runtime/metrics, and pprof's allocs and heap profiles. Reading the allocator in isolation is misleading; reading it alongside the GC pacer (runtime/mgcpacer.go) and the page scavenger (runtime/mgcscavenge.go) is the minimum useful unit.
The audience for this document is engineers who have already read the introductory and senior-track material in this roadmap and want a single place to look up source-file roles, environment variables, runtime APIs, and authoritative design documents.
2. Origins¶
Go's allocator descends from TCMalloc — Thread-Caching Malloc — Google's general-purpose C/C++ allocator originally designed by Sanjay Ghemawat and Paul Menage and described in the paper "TCMalloc: Thread-Caching Malloc" (https://google.github.io/tcmalloc/design.html; the original 2007 design write-up survives at https://gperftools.github.io/gperftools/tcmalloc.html). The Go runtime adopted TCMalloc's three-tier shape — thread cache, central cache, page heap — and the size-class table approach in the earliest Go releases; the m prefix on mcache, mcentral, and mheap traces directly to the TCMalloc naming.
What Go inherited:
- Size classes — a fixed table of allocation sizes (currently 68 non-zero classes ranging from 8 bytes to 32 KiB; see
runtime/sizeclasses.go); each class has a span size that minimises waste. The table is generated byruntime/mksizeclasses.go. - Per-thread caches — small allocations are served from a goroutine-affine free list without lock acquisition; in Go this is the per-P
mcacherather than per-OS-thread. - Central free lists — when a per-thread cache empties, it refills from a central free list (
mcentral) shared across threads; locking is per-size-class rather than global. - Page heap with span coalescing — large allocations and central refills come from a global heap (
mheap) that tracks free pages and coalesces neighbours on free.
What Go changed:
- Per-P rather than per-thread caches. The
mcacheis attached to the runtime processor (P), not the OS thread (M). Goroutines are scheduled onto Ps; an M serves a P; the cache moves with the P. This means the cache count is bounded byGOMAXPROCSrather than by the OS thread count, which matters for systems creating many short-lived goroutines. - Pointer bitmap precision for the GC. TCMalloc is a conservative allocator; the GC has no type information from the allocator. Go's allocator records a pointer-or-not bit per word in
runtime/mbitmap.go, so the garbage collector knows exactly which words in an allocation are pointers and which are scalar data. This is the structural foundation of Go's precise (non-conservative) collector. - Radix-tree page allocator (Go 1.14). The original page allocator used a treap, which became a contention bottleneck at high P counts. Michael Knyszek replaced it with a radix tree (
runtime/mpagealloc.go); the design document is https://go.googlesource.com/proposal/+/master/design/35112-scaling-the-page-allocator.md. The new allocator scales to large heaps with thousands of cores. - GC-aware allocation. Every allocation point participates in the GC pacer's accounting (
runtime/mgcpacer.go) and may trigger an assist (mutator-side scanning work) when the heap grows faster than the collector. This is unique to Go among TCMalloc derivatives. - Background scavenger. Memory that is free at the page-heap level is released back to the OS by a background scavenger goroutine (
runtime/mgcscavenge.go); the heuristic targets a memory-limit-aware steady state. The design is at https://go.googlesource.com/proposal/+/master/design/30333-smarter-scavenging.md. - Experimental arenas (Go 1.20). A user-level bulk allocation API (
runtime/arena.go) exists behindGOEXPERIMENT=arenas. The proposal is https://github.com/golang/go/issues/51317; the experiment is unfinished and may be removed.
The TCMalloc shape is recognisable in Go's allocator, but the GC integration, the precise pointer bitmap, the radix-tree page allocator, and the per-P caches make Go's allocator a distinct design rather than a port.
Three allocation paths. A single mallocgc entry point in runtime/malloc.go dispatches to three internal paths based on size:
- Tiny path (size < 16 bytes, no pointers): combines multiple small non-pointer allocations into a single 16-byte slot. The current tiny block is per-P; allocation increments an offset within the block, and the block is replaced when full. This optimisation matters for strings and small structs; it amortises the per-allocation overhead across multiple logical objects.
- Small path (size <= 32 KiB): consults
mcachefor a free slot in the appropriate size class. If the cache is empty for that class, refills frommcentral; if the central is empty, refills frommheap; if the heap is empty, requests pages from the OS. - Large path (size > 32 KiB): bypasses
mcacheandmcentralentirely, allocates a dedicated span frommheaprounded up to a multiple of the page size (8 KiB on all current platforms).
The three paths share the zeroing protocol, the GC-assist hook, the profile-sample hook, and the pointer-bitmap setup, but their fast paths are distinct. Reading mallocgc is reading three intertwined state machines, not one.
3. Memory model interactions¶
The Go memory model at https://go.dev/ref/mem defines the happens-before relation that constrains which writes a read may observe. The allocator participates in that relation through three guarantees.
Zero-initialisation publication. Every allocation returns memory that has been observably zeroed before the allocator returns it to the caller. The zeroing is either physical (memclr at allocation time) or structural (the page came from a fresh OS mapping that the kernel already zeroed). The memory model treats the return from mallocgc as a synchronising operation: a goroutine reading the newly allocated memory before any other writes to it sees all zeros. There is no observable window of garbage from a previous allocation.
Pointer atomicity. A pointer-sized write to a properly aligned location is atomic with respect to other goroutines: a reader sees either the old value or the new value, never a torn half-word. This is a property of the underlying machine on every Go-supported platform, and the allocator preserves it by returning pointer-aligned memory for every allocation that contains a pointer.
No data race on the allocator's own metadata. Concurrent allocations from different goroutines do not race on the allocator's own state; the per-P mcache, the per-size-class central locks, and the page-heap mutex serialise access to shared structures. The user-visible consequence is that new, make, and composite literal allocation are safe to call concurrently from any goroutine without external synchronisation.
These guarantees apply to allocation only. Once memory is allocated, subsequent writes to it are governed by the regular Go memory model: a happens-before edge is required for one goroutine to observe another's writes. The allocator publishes the zeroed memory; it does not provide any further synchronisation between subsequent users.
Write barriers and allocation. The garbage collector's write barrier (in runtime/mwbbuf.go and runtime/mbarrier.go) interacts with allocation in two places. First, a pointer field initialised by composite-literal allocation does not invoke the write barrier because the target object is by construction unreachable from any GC root at that moment; the runtime inserts a barrier-less store. Second, the GC's mark phase establishes that a newly-allocated object is implicitly black (already marked) during concurrent GC; mutators that store newly-allocated pointers into already-scanned containers therefore do not need the write barrier to discover the new reachability. This invariant (the "allocate-black" rule) is in runtime/mgcmark.go and is one of the foundations of Go's concurrent collector.
Escape analysis interaction. The compiler's escape analysis (in cmd/compile/internal/escape) decides whether each allocation is stack-allocated or heap-allocated. Stack allocations do not enter mallocgc at all; they are part of the function prologue and disappear at function return. The memory model guarantees for stack allocations are weaker only in that they are not observable by other goroutines (the stack is per-goroutine and the object cannot have escaped). For heap allocations, every guarantee above applies.
4. runtime package allocation-related API¶
The public runtime package exposes a small set of functions for inspecting and influencing the allocator. Each is one paragraph; refer to https://pkg.go.dev/runtime for the full signatures.
runtime.MemProfile — returns a snapshot of the per-allocation-site memory profile: a slice of MemProfileRecord entries each carrying allocation count, byte total, and the stack at the allocation point. The profile is the data behind pprof's heap profile. The sampling rate is controlled by runtime.MemProfileRate (default 512 KiB); setting it to 1 records every allocation, setting it to 0 disables profiling.
runtime.SetFinalizer — associates a finalizer function with an object. The finalizer runs in a separate goroutine after the object becomes unreachable but before its memory is reclaimed; the object becomes reachable again for the duration of the finalizer call. The implementation lives in runtime/mfinal.go. Finalizers are unreliable for resource management — they may run late, may not run at all on program exit, and resurrect the object — and the standard guidance is to use explicit Close methods instead.
runtime.KeepAlive — ensures the argument is reachable at the point of the call, even if the compiler would otherwise determine the value is dead. Used in CGo bridges and finalizer-protected code where a pointer is passed to C and the Go GC must not reclaim the backing object before the C call returns. The function does no actual work; its presence informs the compiler's liveness analysis.
runtime.GC — runs a garbage collection synchronously and blocks until it completes. Used in tests, benchmarks, and diagnostic code; production code rarely calls it because the runtime's pacer manages GC timing. The function does not free OS memory directly; freeing is done lazily by the scavenger.
runtime/debug.FreeOSMemory — forces a GC followed by an immediate attempt to release as much memory as possible back to the OS. The function exists for diagnostic and shutdown-quiescing scenarios; production servers usually let the background scavenger do this work over time. The implementation is in runtime/debug/garbage.go and calls into runtime.scavenge_m.
runtime/debug.SetMemoryLimit — sets the soft memory limit at runtime (the same value can be set with the GOMEMLIMIT environment variable). The limit is soft: the runtime will work harder to avoid exceeding it but does not guarantee an OOM-free path. Section 7 covers the semantics in detail.
runtime.ReadMemStats — populates a caller-provided MemStats struct with the current allocator state. The call is a stop-the-world snapshot on older Go versions and a coordinated read on Go 1.16+. Section 8 covers the field reference.
runtime.MemProfileRate — the sampling rate for memory profiling, in bytes; the default is 524288 (one sample per 512 KiB allocated on average). Setting it to 1 samples every allocation (extremely expensive but exact); setting it to 0 disables profiling entirely. Must be set before any allocation runs to be reliable; changing it mid-run affects only subsequent samples.
runtime.GOMAXPROCS — not directly an allocator function, but indirectly controls the number of mcache instances: there is one per P, and GOMAXPROCS sets the P count. Increasing it adds per-P cache memory; decreasing it returns caches and their spans to the central free lists.
5. runtime/metrics allocator-related metrics¶
The runtime/metrics package, introduced in Go 1.16, exposes runtime state as named, typed metrics suitable for export to Prometheus, OpenTelemetry, or any pull-style monitoring system. The full list is at https://pkg.go.dev/runtime/metrics and is also exposed at runtime via metrics.All(). The allocator-related subset:
| Metric | Type | Description |
|---|---|---|
/memory/classes/total:bytes | uint64 | All memory mapped by the runtime: heap, stacks, runtime metadata, profiling buffers. The closest analogue to MemStats.Sys. |
/memory/classes/heap/objects:bytes | uint64 | Memory occupied by live heap objects (excludes whitespace inside spans). Matches MemStats.HeapAlloc. |
/memory/classes/heap/unused:bytes | uint64 | In-use spans whose slots are not currently allocated; reusable without OS interaction. |
/memory/classes/heap/released:bytes | uint64 | Memory returned to the OS but kept reserved; counts toward virtual size but not RSS once paged out. |
/memory/classes/heap/free:bytes | uint64 | Spans the runtime owns but has not handed back to the OS; available for new allocations. |
/memory/classes/heap/stacks:bytes | uint64 | Memory in use by goroutine stacks; allocated from the heap but managed separately. |
/memory/classes/metadata/mcache/*:bytes | uint64 | Per-P cache structures and their slack. |
/memory/classes/metadata/mspan/*:bytes | uint64 | Span descriptor memory. |
/memory/classes/metadata/other:bytes | uint64 | All other runtime bookkeeping not separately accounted. |
/memory/classes/os-stacks:bytes | uint64 | Memory used by OS-thread stacks (M stacks), distinct from goroutine stacks. |
/memory/classes/profiling/buckets:bytes | uint64 | Memory used by the profile-bucket hash tables. |
/gc/heap/allocs:bytes | uint64 | Cumulative bytes allocated to the heap since program start (monotonically increasing). |
/gc/heap/allocs:objects | uint64 | Cumulative object count allocated to the heap. |
/gc/heap/frees:bytes | uint64 | Cumulative bytes freed by the GC. |
/gc/heap/frees:objects | uint64 | Cumulative object count freed. |
/gc/heap/goal:bytes | uint64 | The GC's current target heap size for the next cycle. |
/gc/heap/live:bytes | uint64 | Bytes the most recent GC determined were live (the new "denominator" the pacer uses). |
/gc/heap/objects:objects | uint64 | Current count of live heap objects. |
/gc/heap/tiny/allocs:objects | uint64 | Tiny-allocator allocations (objects < 16 bytes that share a 16-byte slot). |
/gc/scan/heap:bytes | uint64 | Bytes scanned by the most recent GC mark phase. |
/gc/scan/stack:bytes | uint64 | Bytes scanned across goroutine stacks. |
/gc/scan/globals:bytes | uint64 | Bytes scanned across the BSS and data segments. |
/gc/limiter/last-enabled:gc-cycle | uint64 | The most recent GC cycle in which the CPU limiter ran. |
/sched/gomaxprocs:threads | uint64 | The current GOMAXPROCS; the cache count for mcache. |
For convention reasons every byte-valued metric ends in :bytes and every count-valued metric ends in :objects or a similar unit suffix. The metrics.Sample API reads metrics in bulk; the cost is significantly lower than ReadMemStats for the same information because it is a coordinated rather than stop-the-world read.
6. GODEBUG knobs¶
The GODEBUG environment variable carries a comma-separated list of key=value pairs that influence runtime behaviour. The allocator-related knobs (also documented at https://pkg.go.dev/runtime):
| Knob | Values | Description |
|---|---|---|
allocfreetrace=1 | 0 (off, default), 1 (on) | Trace every allocation and free with a stack to stderr. Catastrophically slow; useful only on tiny test programs to confirm where allocations happen. |
gctrace=1 | 0, 1, 2 | Print a GC summary at the end of each cycle: heap-before, heap-after, goal, wall and CPU time, scan rate. Value 2 adds extra detail. The canonical first-step diagnostic. |
scavtrace=1 | 0, 1 | Trace the page scavenger's decisions: how many pages it released, the working-set estimate, the memory-limit setpoint. Useful when investigating RSS over time. |
madvdontneed=0 | 0, 1 | On Linux, controls whether the scavenger uses MADV_DONTNEED (1, immediate decommit, slightly higher CPU) or MADV_FREE (0, default on Linux 4.5+, lazy decommit, lower CPU). On non-Linux it is a no-op. |
gcpacertrace=1 | 0, 1 | Print pacer-internal decisions per GC cycle: the assist ratio, the trigger ratio, the live-heap estimate. Used to diagnose pacer-related regressions. |
gccheckmark=1 | 0, 1 | Run a stop-the-world re-scan after each GC and panic if any object the concurrent collector marked dead is still reachable. Catches GC correctness bugs; never run in production. |
gcstoptheworld=1 | 0, 1, 2 | Force GC to be fully stop-the-world (1) or stop-the-world with sweeping disabled (2); a debugging knob, not a tuning knob. |
efence=1 | 0, 1 | Allocate every object on its own page surrounded by guard pages; segfaults on out-of-bounds access. Used to find memory-corruption bugs in cgo or unsafe code. |
invalidptr=1 | 0, 1 | When 0, the GC tolerates invalid pointers in heap memory instead of panicking. Default is 1; setting 0 is reserved for diagnosing cgo bugs. |
Example gctrace=1 output (one line per GC cycle):
gc 1 @0.005s 2%: 0.005+0.18+0.001 ms clock, 0.04+0.05/0.13/0.32+0.012 ms cpu, 4->4->1 MB, 5 MB goal, 0 MB stacks, 0 MB globals, 8 P
The interpretation: cycle number 1, started 5 ms after process start, 2% of CPU spent on GC so far; pause times for each phase; heap was 4 MB before, 4 MB during sweep, 1 MB live; goal was 5 MB; 8 Ps active. The format is documented at https://pkg.go.dev/runtime#hdr-Environment_Variables.
Example scavtrace=1 output:
The scavenger released 50 KiB in this round, total released 256 KiB since program start, the page-heap is 12.5% utilised.
Setting multiple knobs. GODEBUG accepts comma-separated pairs: GODEBUG=gctrace=1,scavtrace=1,madvdontneed=1. Order is not significant. Invalid keys are silently ignored; invalid values produce a runtime warning at startup on most knobs. Some knobs (notably gccheckmark) impose large runtime overhead and must be removed before deploying to production.
Knobs not listed here. The full list of GODEBUG keys spans the runtime, the net package, the cryptography stack, and the cgo bridge; https://pkg.go.dev/runtime#hdr-Environment_Variables and the Go release notes document the complete set. The allocator-related subset above is stable across recent releases; non-allocator knobs change more frequently.
GODEBUG compatibility. Unlike most runtime details, GODEBUG keys are part of a partial compatibility regime: keys documented as such in https://go.dev/doc/godebug are guaranteed to remain available for the lifetime of the major Go release in which they were introduced, even when the underlying default changes. The default-changing knobs are how Go quietly evolves runtime behaviour while preserving an opt-out for users who depend on the old behaviour.
7. GOMEMLIMIT¶
GOMEMLIMIT, added in Go 1.19 (proposal https://github.com/golang/go/issues/48409), sets a soft cap on the total memory the Go runtime will use. The value is bytes, with optional KiB, MiB, GiB, TiB suffixes (GOMEMLIMIT=2GiB); a negative value or off disables the limit. The same value can be set at runtime with runtime/debug.SetMemoryLimit.
Exact semantics:
- The limit applies to the total of all
runtime/metrics/memory/classes/total:bytes— heap, stacks, runtime metadata, profiling — minus/memory/classes/heap/released:bytes(memory already returned to the OS). It is not a limit onHeapAllocalone. - The limit is soft. The runtime will spend more CPU on GC to stay under it, but if live data alone exceeds the limit, the runtime continues to allocate; it does not OOM-kill itself. The protection against runaway is that GC will reach effectively 100% of CPU, which manifests as severe slowdown rather than crash. In production this is preferable to crash but still requires monitoring.
- The pacer treats
GOMEMLIMITas an additional constraint alongsideGOGC. IfGOGCwould trigger GC at a heap size lower than the limit,GOGCwins; ifGOGCwould let the heap grow beyond the limit, the pacer schedules GC sooner. - The CPU limiter (Go 1.20+) caps GC CPU usage at approximately 50% over a sliding window to prevent the runtime from spending all its time in GC when the limit is too tight; when the limiter activates, the heap is allowed to exceed
GOMEMLIMITrather than the program becoming unresponsive.runtime/metrics./gc/limiter/last-enabled:gc-cyclereports the most recent activation. - The scavenger uses the limit as its release target: it returns memory to the OS aggressively when usage is near or above the limit, conservatively when usage is well below.
Practical guidance. Set GOMEMLIMIT to roughly 90 to 95% of the container memory limit. The remaining 5 to 10% is the headroom that absorbs the limit's soft nature and the moment-to-moment variation between GCs. Setting it equal to the cgroup limit invites OOM-kill from the kernel; leaving it unset means the runtime treats memory as unbounded and may attempt to grow into swap or trigger the kernel OOM-killer.
GOMEMLIMIT does not replace GOGC; the two are complementary. GOGC controls the steady-state heap-growth ratio; GOMEMLIMIT is a ceiling beyond which the steady state is overridden. Many production deployments set both: GOGC=100 GOMEMLIMIT=8GiB is a common shape.
Interaction with the scavenger. Below the limit, the scavenger releases memory lazily on a working-set heuristic: it tracks the maximum heap size observed over a recent window and returns pages exceeding that figure. Near or above the limit, the scavenger releases aggressively, treating every reclaimable page as a candidate. The scavtrace=1 output reveals which mode is active: a steady stream of releases indicates the aggressive mode; sporadic releases indicate the lazy mode.
Interaction with the GC pacer. The pacer's primary input is the GOGC ratio applied to the live heap (the "next GC" target). When GOMEMLIMIT is set and the live heap plus stacks plus metadata approaches the limit, the pacer reduces the trigger ratio: GC starts earlier, more CPU is spent in mark, and the steady-state heap stays below the limit. The CPU limiter is the safety valve: it caps GC CPU at approximately 50% so that the program continues to make forward progress even when the limit is impossibly tight.
8. MemStats reference¶
runtime.MemStats is the original allocator-state snapshot, documented at https://pkg.go.dev/runtime#MemStats. The fields most relevant to allocator behaviour:
| Field | Type | Description |
|---|---|---|
Alloc | uint64 | Same as HeapAlloc. |
TotalAlloc | uint64 | Cumulative bytes allocated to the heap; monotonically increasing. |
Sys | uint64 | Total bytes obtained from the OS; sum of *Sys fields. The closest single-number proxy for RSS, modulo released memory. |
Lookups | uint64 | Number of pointer lookups by the runtime; for runtime-developer diagnostics. |
Mallocs | uint64 | Cumulative heap-object allocations. |
Frees | uint64 | Cumulative heap-object frees. Mallocs - Frees equals HeapObjects. |
HeapAlloc | uint64 | Bytes of allocated heap objects; the "live + recently dead but not yet swept" figure. The most commonly cited allocator-pressure metric. |
HeapSys | uint64 | Bytes obtained from the OS for the heap; includes virtual address space reserved but not necessarily resident. |
HeapIdle | uint64 | Bytes in idle (unused) spans; available for allocation or scavenging. |
HeapInuse | uint64 | Bytes in in-use spans; the working set. |
HeapReleased | uint64 | Bytes returned to the OS via the scavenger; reduces RSS but counts toward HeapSys. |
HeapObjects | uint64 | Current count of live heap objects. |
StackInuse | uint64 | Bytes in goroutine stack spans. |
StackSys | uint64 | Bytes obtained from the OS for goroutine stacks. |
MSpanInuse | uint64 | Bytes used by allocated mspan descriptors. |
MSpanSys | uint64 | Bytes obtained from the OS for mspan storage. |
MCacheInuse | uint64 | Bytes used by allocated mcache structures. |
MCacheSys | uint64 | Bytes obtained from the OS for mcache storage. |
BuckHashSys | uint64 | Bytes used by the profiling bucket hash table. |
GCSys | uint64 | Bytes used by GC metadata. |
OtherSys | uint64 | Bytes used by other runtime allocations. |
NextGC | uint64 | Target heap size for the next GC cycle. |
LastGC | uint64 | Wall clock of the most recent GC, in nanoseconds since the epoch. |
PauseTotalNs | uint64 | Cumulative GC pause time. |
PauseNs | [256]uint64 | Circular buffer of recent GC pause durations. |
NumGC | uint32 | Count of completed GC cycles. |
NumForcedGC | uint32 | Count of GC cycles forced by runtime.GC. |
GCCPUFraction | float64 | Fraction of CPU time spent in GC since program start. |
EnableGC | bool | Always true except in trace-replay tooling. |
DebugGC | bool | Reserved; not currently meaningful. |
BySize | [61]MemStatsBySize | Per-size-class counts: Size, Mallocs, Frees. |
For new code, prefer runtime/metrics: it is cheaper, more granular, and the field names are stable across releases. MemStats remains useful for one-shot diagnostics and for tooling that predates runtime/metrics.
Quick interpretation of common shapes. A program with HeapAlloc of 1 GiB, HeapSys of 2 GiB, and HeapReleased of 500 MiB has 1 GiB of live data, holds 2 GiB of address space, and has returned 500 MiB to the OS (so resident-set is approximately 1.5 GiB). If HeapIdle - HeapReleased is large, the runtime is sitting on a lot of freed memory that has not yet been scavenged; either the scavenger has not had time, or it is in lazy mode. If Mallocs - Frees is growing linearly, the program is leaking objects. If NumGC is growing rapidly relative to wall time, GC is under pressure; check GCCPUFraction.
Differences from runtime/metrics. MemStats.HeapAlloc corresponds to /memory/classes/heap/objects:bytes plus some accounting differences; HeapSys corresponds approximately to /memory/classes/heap/objects + /memory/classes/heap/unused + /memory/classes/heap/free + /memory/classes/heap/released. The two surfaces report consistent values but partition them differently; the runtime/metrics partition is more useful for understanding where memory is held.
9. Authoritative source files¶
The allocator's source is in src/runtime/ of the Go repository at https://go.googlesource.com/go/+/refs/heads/master/src/runtime/. The files most central to allocation:
| File | Role |
|---|---|
malloc.go | Defines mallocgc, the single entry point used by new, make, composite literals, and conversion. Coordinates with the GC pacer (gcAssistAlloc), runs zeroing, dispatches to tiny / small / large paths. The "front door" of the allocator. |
mcache.go | Per-P thread cache: an array of free lists keyed by size class. mcache.refill pulls a fresh span from the central; mcache.releaseAll returns slots on P shutdown. |
mcentral.go | Per-size-class central data structure. Two linked lists: partial (spans with at least one free slot) and full (spans with no free slots). mcentral.cacheSpan is the path that hands a span to an mcache requesting a refill. |
mheap.go | The global page heap. Owns the page allocator, the span allocator, and the large-object path. mheap.alloc is the request entry point for both span refills (from mcentral) and large allocations (directly from mallocgc). |
mpagealloc.go | The radix-tree page allocator introduced in Go 1.14. Replaces the older treap; scales to large heaps. Companion files mpagealloc_64bit.go and mpagealloc_32bit.go carry the platform specialisations. |
mpagecache.go | Per-P page cache: a small number of free pages cached at the page-heap level for low-contention small allocations. |
sizeclasses.go | The generated size-class table: 68 non-zero classes, their span sizes, and the number of objects per span. The generator is runtime/mksizeclasses.go. |
mbitmap.go | The pointer bitmap: one bit per word indicating "this word contains a pointer". The structural foundation of precise GC. The file also handles allocation-side bitmap setup. |
mspan.go | Span descriptor: a contiguous range of pages serving one size class (or one large object). mspanList and the allocation bitmap (allocBits, gcmarkBits) are defined here. |
mgcscavenge.go | The background scavenger. Decides when to return pages to the OS; interacts with GOMEMLIMIT and the working-set heuristic. |
mfinal.go | Finalizer queue and execution. SetFinalizer registers; the scavenger and the sweeper enqueue finalizers; a dedicated goroutine runs them. |
arena.go | The experimental user-level arena allocator (Go 1.20+, behind GOEXPERIMENT=arenas). Carries the New, Free, and Reset operations. |
mfixalloc.go | Fixed-size allocator used by the runtime itself for mspan, mcache, specialfinalizer, and similar internal records. Not used for heap user objects. |
mranges.go | Address-range tracking used by the page allocator and the scavenger. |
The companion garbage-collector files (mgc.go, mgcmark.go, mgcsweep.go, mgcpacer.go) interact constantly with the allocator and are required reading for anyone modifying allocation paths.
Generated and platform-specific files. Several files are generated or carry platform-specific specialisations:
| File | Role |
|---|---|
mfixalloc.go | Internal fixed-size allocator for runtime bookkeeping records (mspan, mcache, special*). Not the user-allocation path. |
msan*.go / asan*.go | Hooks for the C/C++ MemorySanitizer and AddressSanitizer when Go is built with -msan or -asan; allow Go-allocated memory to be tracked by the sanitiser. |
mksizeclasses.go | The generator for sizeclasses.go; encodes the size-class waste-budget calculation. |
malloc_test.go | Allocator unit tests; useful as executable documentation of intended behaviour. |
mpagealloc_64bit.go, mpagealloc_32bit.go | Platform specialisations of the radix tree; the 64-bit version uses a five-level tree, the 32-bit version a smaller arrangement. |
mem_linux.go, mem_darwin.go, mem_windows.go, mem_bsd.go, mem_plan9.go | OS-specific sysAlloc, sysUsed, sysUnused, sysFree, sysMap implementations; the abstraction layer between the page allocator and mmap (or its OS equivalent). |
signal_unix.go, signal_windows.go | Signal handlers that interact with allocation in the sense that allocations during signal handling are forbidden; the runtime enforces this. |
The mem_*.go files are the only place the allocator touches the OS. On Linux they call mmap, munmap, and madvise; on Darwin they use the Mach VM API; on Windows they use VirtualAlloc and VirtualFree. The abstraction is intentionally thin because the allocator wants direct control over page-level mapping decisions.
10. Notable design documents and proposals¶
The allocator's evolution is recorded in design documents at https://go.googlesource.com/proposal/+/master/design/ and in tracking issues at https://github.com/golang/go/issues. The most important:
| Reference | Topic |
|---|---|
| TCMalloc design (https://google.github.io/tcmalloc/design.html) | The thread-caching, central-free-list, page-heap shape Go inherited. |
35112-scaling-the-page-allocator.md | The Go 1.14 radix-tree page allocator that replaced the treap; author Michael Knyszek; resolves contention at high P counts. |
48409 (GOMEMLIMIT, Go 1.19) | The soft memory limit; defines the contract with the pacer and the scavenger. |
47657 (Arenas, ongoing) | The experimental user-level arena API. Behind GOEXPERIMENT=arenas; may be removed if the design does not stabilise. |
30333-smarter-scavenging.md | The background scavenger redesign; introduces the working-set heuristic. |
44309 (Tail latency improvements) | Pacer rewrite that reduced GC tail latency; ships in Go 1.18. |
12800 (Concurrent sweep) | The original concurrent sweep design that overlapped sweeping with mutator execution. |
17503 (Non-cooperative goroutine preemption) | Not allocator-specific but interacts with stack growth and GC scanning. |
46787 (Memory model formalisation) | Clarifies the happens-before guarantees that allocation participates in. |
mksizeclasses.go comments | Carries the rationale for the size-class table and the waste-budget calculation. |
Reading the proposal text rather than the implementation alone is the fastest way to understand why a piece of the allocator looks the way it does. Many invariants in the source are documented only in the proposal that introduced the code.
11. Compatibility¶
The Go 1 compatibility promise covers the language specification and the public APIs of packages in the standard library. It does not cover:
- The internal layout of
runtimedata structures (mcache,mcentral,mheap,mspan). - The size-class table; classes have been added, removed, and resized across releases.
- The page allocator's algorithm; the treap-to-radix-tree transition in Go 1.14 changed performance characteristics dramatically.
- The scavenger's release schedule; aggressiveness has been retuned in nearly every release.
- The pacer's decision function; rewrites in Go 1.5, 1.10, 1.18, and 1.19.
- The exact bytes reported by
runtime.MemStatsfields; the definitions are stable, the values vary with implementation. - The output format of
GODEBUG=gctrace=1and similar diagnostic knobs.
The compatibility-affecting surface is the runtime package public API: MemStats field names, ReadMemStats, SetFinalizer, KeepAlive, MemProfile, MemProfileRate, and the documented GOMEMLIMIT semantics. Code that depends on the bytes-exact behaviour of any of these is depending on accident, not contract.
For tools that need stable interfaces over time, runtime/metrics is the right substrate: every metric carries a name and a unit, and removals follow a deprecation cycle.
What "stable" means in practice for MemStats. The field names listed in section 8 have been stable since Go 1.0 (with additions for HeapReleased, NumForcedGC, GCCPUFraction, and a few others). The values in those fields reflect the current implementation: HeapAlloc in Go 1.5 had a different meaning than in Go 1.20 because the allocator's accounting changed. Code that compares MemStats snapshots from different Go versions is comparing apples and oranges; code that uses a single snapshot to size a process at runtime is on solid ground.
Removed and deprecated fields. MemStats.BySize is still present but is increasingly less useful as the size-class table changes between releases. EnableGC and DebugGC are vestigial; both have been deprecated as meaningful in modern releases. New code should not branch on either.
Forward compatibility. New fields may be added to MemStats in any release; the struct is exported by name, so reflection-based or binary.Read-style consumers will silently get more bytes back than they expect. The conventional shape — var ms runtime.MemStats; runtime.ReadMemStats(&ms) — is forward-compatible because the struct's size is determined by the linker at build time.
Why this matters. Most production memory issues in Go programs are diagnosed by reading three numbers — HeapAlloc, HeapSys, HeapReleased — alongside the GOMEMLIMIT setting and the container's memory limit. The other fields refine the picture; the three above answer "is the program leaking, is it sitting on freed memory, is the OS aware that the memory is unused".
12. Reading order for the source¶
A recommended path for reading the allocator end to end:
runtime/sizeclasses.go— the size-class table. Understanding the table is prerequisite to understanding whymallocgcbranches the way it does.runtime/malloc.go, focusing onmallocgc— the top-level entry. Read the tiny-allocator path, the small-allocator path, the large-allocator path, and the assist-and-zeroing interactions in that order.runtime/mcache.go— the per-P cache and the refill protocol.runtime/mcentral.go— the central free list and the cache-span interaction.runtime/mheap.go— the global heap, the large-allocation path, and the entry into the page allocator.runtime/mpagealloc.go— the radix-tree page allocator; densest file in the allocator; pair with proposal 35112 for context.runtime/mbitmap.go— the pointer bitmap; the structural foundation of precise GC.runtime/mspan.go— the span descriptor and its allocation bitmap.runtime/mgcscavenge.go— the scavenger; how memory returns to the OS.runtime/mgcpacer.go— the pacer; the bridge between allocator pressure and GC scheduling.runtime/mfinal.go— finalizers; small file, distinct mechanism.runtime/arena.go— the experimental user arena, optional reading.
Plan on a week of focused reading to get past the first three files, a month to be comfortable across all twelve. The comments in the files are dense and accurate; the proposals fill in the design rationale that the comments assume.
Prerequisite background. Before opening runtime/malloc.go, read the introductory and senior-track files in this roadmap's 04-memory-allocator/ folder (junior, middle, senior, performance, internals, interview, optimize, professional). Also review:
- The Go scheduler at a sketch level (P, M, G; https://go.dev/src/runtime/proc.go), because
mcacheis per-P and the allocator's locking discipline mirrors the scheduler's. - The basics of the tri-color mark-and-sweep collector and the concurrent-marking invariant.
- The format of the size-class table in
sizeclasses.go(rungo run runtime/mksizeclasses.goto regenerate). - The page size on the target platform (8 KiB on all current Go-supported architectures; the
_PageSizeconstant inruntime/malloc.go).
Tooling for the source. Running the runtime tests requires GOFLAGS=-tags=goexperiment.something for some experiments; the comments at the top of each file list build-tag requirements. The runtime is its own package and cannot be unit-tested in isolation from the rest of the runtime; tests live in runtime/*_test.go and runtime/runtime-gdb_test.go. The cmd/runtime/runtime binary does not exist; the runtime is linked into every Go program.
Reading the change history. Many allocator decisions are documented only in commit messages. git log --follow src/runtime/malloc.go and the equivalents for other files reveal the chronology of changes; pair each substantial commit with the linked issue or design document in the Go issue tracker.
13. Bug reporting¶
Bugs against the Go memory allocator are filed at https://github.com/golang/go/issues with the label runtime. The triage team welcomes:
- Reproducers showing pathological allocation behaviour: high CPU spent in GC, RSS not falling under load reduction, unexpected
MemStatsvalues. gctrace=1andscavtrace=1output captured during the bad behaviour.- The exact Go version (
go version),GOOS,GOARCH, kernel version, container memory limit, andGOGC/GOMEMLIMITvalues. - A self-contained program when possible; a private reproducer with a workaround steering when not.
Performance regressions between Go releases are explicitly in scope; bisect with git bisect between the offending releases on the Go source repository if possible. The release notes at https://go.dev/doc/devel/release list runtime-affecting changes per release.
For confidential reports involving memory-safety bugs reachable from untrusted input, follow the security policy at https://go.dev/security/policy; the runtime team treats allocator and GC correctness bugs as security-grade when they cross trust boundaries.
14. Further reading¶
- Go runtime package documentation: https://pkg.go.dev/runtime
- Go
runtime/metricspackage documentation: https://pkg.go.dev/runtime/metrics - Go memory model: https://go.dev/ref/mem
- TCMalloc design write-up: https://google.github.io/tcmalloc/design.html
- Go 1.14 page allocator design: https://go.googlesource.com/proposal/+/master/design/35112-scaling-the-page-allocator.md
- GOMEMLIMIT proposal: https://github.com/golang/go/issues/48409
- Arenas proposal: https://github.com/golang/go/issues/51317
- Scavenger redesign: https://go.googlesource.com/proposal/+/master/design/30333-smarter-scavenging.md
- Source root: https://go.googlesource.com/go/+/refs/heads/master/src/runtime/
- Issue tracker (allocator-related label): https://github.com/golang/go/labels/runtime
The Go memory allocator is one of the best-documented allocators in any production language, but the documentation is scattered across the source, the proposals, and the release notes. Treat this file as the index; read the source and the proposals for the contract.
15. Glossary¶
| Term | Meaning |
|---|---|
mcache | Per-P thread cache of free objects keyed by size class; the fast path for small allocations; no locking required because access is goroutine-affine to its P. |
mcentral | Per-size-class central free list shared across Ps; serves cache refills under a per-class lock. |
mheap | Global page heap; manages free pages, span allocation, and large-object allocation; entry to the page allocator. |
mspan | Span descriptor: a contiguous range of pages serving one size class or one large object; carries allocation bits and GC mark bits. |
| Size class | One of 68 fixed allocation sizes from 8 bytes to 32 KiB; defined in runtime/sizeclasses.go; chosen to minimise internal fragmentation. |
| Tiny allocator | A sub-path within mallocgc that combines multiple sub-16-byte non-pointer allocations into a single 16-byte slot; per-P state. |
| Page allocator | The radix-tree allocator in runtime/mpagealloc.go that tracks free pages at the address-space level; replaces the older treap (Go 1.14). |
| Scavenger | The background goroutine in runtime/mgcscavenge.go that returns free pages to the OS via madvise. |
| Pacer | The GC pacing logic in runtime/mgcpacer.go that schedules collections based on heap growth and GOMEMLIMIT. |
| Write barrier | Compiler-inserted code that runs on pointer stores during concurrent GC; preserves the tri-colour mark invariant. |
| Allocate-black | The invariant that newly allocated objects are implicitly marked during GC; permits mutator-side allocation without a write barrier on initial pointer fields. |
| Pointer bitmap | One bit per word stored alongside the heap indicating whether the word contains a pointer; the foundation of precise GC. |
| Span | Synonym for mspan; the smallest unit of page-level bookkeeping. |
| Soft memory limit | GOMEMLIMIT's semantics: the runtime works harder to stay under the limit but does not crash when live data exceeds it. |
| Arena | The experimental user-level bulk-allocation API in runtime/arena.go; behind GOEXPERIMENT=arenas. |
| Working set | The estimate the scavenger maintains of recent peak heap usage; pages above this figure are candidates for return to the OS. |