Backend Development 15 min read

Understanding Go Stack Memory Management

The article explains how Go’s runtime manages goroutine stacks—starting with a 2 KB initial size, evolving from segmented to continuous stacks, detailing allocation via stackpool and stackLarge, expansion with runtime.newstack, shrinkage by the garbage collector, and the internal structures that coordinate these processes.

Tencent Cloud Developer
Tencent Cloud Developer
Tencent Cloud Developer
Understanding Go Stack Memory Management

This article, originally published in the Tencent Cloud Developer Community, explains how Go manages stack memory for goroutines. It covers the evolution of stack size, the transition from segmented stacks to continuous stacks, and the internal mechanisms used by the Go runtime.

Stack Basics

Each goroutine has its own stack, initially 2 KB in size. The stack stores function parameters and local variables and is automatically allocated and freed by the compiler.

The minimum stack size has changed across Go versions:

v1.0–v1.1: minimum 4 KB

v1.2: minimum 8 KB

v1.3: switched from segmented to continuous stack

v1.4–v1.19: minimum reduced to 2 KB

Segmented Stack

When a goroutine’s call depth grows, the runtime creates a new, non‑contiguous stack segment via runtime.morestack and runtime.newstack . These segments are linked together in a doubly‑linked list, allowing the runtime to locate a continuous slice when needed.

Advantages:

Memory is allocated on demand for the specific goroutine.

Disadvantages:

If a stack is almost full, every function call triggers a stack expansion, and the subsequent return triggers a shrink. This repeated allocation/deallocation is known as the *hot split* problem.

To mitigate hot split, Go 1.2 increased the initial stack size from 4 KB to 8 KB.

Continuous Stack

A continuous stack solves the problems of the segmented stack. When the current stack is insufficient, the runtime allocates a new stack that is twice as large as the old one, copies all data with runtime.copystack , updates pointers, and frees the old stack.

The expansion process involves:

Calling runtime.newstack to allocate a larger stack.

Using runtime.copystack to copy the old stack’s contents.

Redirecting pointers from the old stack to the new one.

Calling runtime.stackfree to release the old stack.

Stack Management Structures

Two global variables manage stack memory:

runtime.stackpool – cache for stacks smaller than 32 KB.

runtime.stackLarge – cache for stacks 32 KB or larger.

(1) StackPool

Used for stacks ≤ 32 KB. The stack size must be a power of two, with a minimum of 2 KB. On Linux, the pool provides lists for 2 KB, 4 KB, 8 KB, and 16 KB spans.

// Global pool of spans that have free stacks.
// Stacks are assigned an order according to size.
// order = log_2(size/FixedStack)
var stackpool [_NumStackOrders]struct {
    item stackpoolItem
    _    [cpu.CacheLinePadSize-unsafe.Sizeof(stackpoolItem{})%cpu.CacheLinePadSize]byte
}

type stackpoolItem struct {
    mu   mutex
    span mSpanList
}

type mSpanList struct {
    first *mspan // first span in list, or nil if none
    last  *mspan // last span in list, or nil if none
}

(2) StackLarge

Handles stacks ≥ 32 KB. It is an array of 25 mSpan lists, each representing a size that doubles the previous one, starting from 8 KB.

// Global pool of large stack spans.
var stackLarge struct {
    lock mutex
    free [heapAddrBits-pageShift]mSpanList // free lists by log_2(s.npages)
}

type mSpanList struct {
    first *mspan // first span in list, or nil if none
    last  *mspan // last span in list, or nil if none
}

(3) Memory Allocation

Each P (processor) has a local cache runtime.mcache.stackcache to reduce lock contention. Allocation steps:

For stacks < 32 KB, the runtime first tries the local cache.

If the local list is empty, it obtains a 16 KB block from stackpool and fills the cache.

If stackpool is also empty, a 32 KB span is allocated from the heap and split.

For stacks ≥ 32 KB, the required number of pages is calculated, the logarithm base‑2 of that number selects an index in stackLarge , and a free span is taken. If none is available, a new span is allocated from the heap.

(4) Memory Release

If a goroutine’s stack never grew (remains 2 KB), the goroutine is placed in the free‑G queue with a stack.

If the stack grew, it is freed and the goroutine is moved to the no‑stack queue. The GC later releases these stacks during the mark‑root phase.

Stacks < 32 KB are returned to the local cache first; excess cache memory (> 32 KB) is pushed back to stackpool . If the cache is unavailable, the stack goes directly to stackpool .

Stacks ≥ 32 KB are returned to stackLarge unless the GC is currently freeing memory, in which case they are returned to the heap.

(5) Stack Expansion

The compiler inserts a call to runtime.morestack before most function calls. If the current stack is insufficient, runtime.newstack creates a new stack twice the size of the old one (subject to the maximum stack limit). The runtime then copies the old stack with runtime.copystack and frees the old stack.

(6) Stack Shrinkage

When a goroutine’s stack usage falls below ¼ of its capacity, the GC may shrink the stack using runtime.shrinkstack . The new stack size is half of the old one, but never below the minimum 2 KB. Shrinkage also uses runtime.copystack to move data and adjust pointers. The only initiator of stack shrinkage is the GC; if it cannot shrink immediately, it sets a flag ( g.preemptShrink ) so the goroutine will shrink before yielding the CPU.

References:

GoLang Stack Memory Management

White‑paper: Go Memory Management – Stack Chapter

Memory ManagementConcurrencyGoruntimegoroutineStack Memory
Tencent Cloud Developer
Written by

Tencent Cloud Developer

Official Tencent Cloud community account that brings together developers, shares practical tech insights, and fosters an influential tech exchange community.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.