Memory Layout Deep Dive¶
Stack: function call frame contains return address, saved registers, local variables. Grows downward on x86-64. 8MB limit guarded by a read-only page. O(1) allocation (decrement SP). Freed automatically on return. Go: contiguous growable stack — full stack triggers 2× reallocation and frame copy.
Heap: malloc calls brk() (small) or mmap() (large). Pages demand-paged — physically allocated only on first write. free() returns to allocator's free list, not immediately to OS.
Worked example — stack growth: a recursive function keeps a 1KB local array per frame and recurses 10,000 deep — roughly 10MB of stack needed. A Go goroutine starts with an 8KB stack; as recursion deepens, the runtime detects the stack is full and reallocates at double the size each time (8KB → 16KB → 32KB → ... → 16MB), copying the existing frames to the new location and fixing up any pointers into the stack. A C thread with a fixed 8MB stack (Linux default) hits the same 10MB requirement, finds no such mechanism, runs into the guard page below the stack, and crashes with SIGSEGV instead of growing.
Worked example — heap fragmentation: a process mallocs 1,000 objects of 100 bytes (100KB total), then frees every other one — 500 scattered 100-byte holes now sit in the free list, non-contiguous. A later malloc(300) doesn't fit in any single 100-byte hole, even though 500 × 100 = 50KB of "free" memory technically exists; the allocator has to request fresh memory from the OS instead of reusing what's already been freed. This is why long-running processes with many small alloc/free cycles of varying sizes can show heap RSS creeping upward even when the live object count is stable — freed memory is available to the allocator, but not necessarily in a usable shape.