PPrepLearn
Progress · 0/13 sections

02 — Memory Allocation, GC & Size Classes

7 min read

02 — Memory Allocation, GC & Size Classes

Go memory kaise allocate karta hai aur Garbage Collector kaise kaam karta hai


Memory Layout — Big Picture

┌───────────────────────────────────────────────────┐
│                 Virtual Address Space              │
├───────────────────────────────────────────────────┤
│  ┌─────────────────────────────────────────────┐  │
│  │              HEAP                            │  │
│  │  (dynamic allocation — GC managed)           │  │
│  │  ┌────────┐ ┌────────┐ ┌────────┐           │  │
│  │  │ Span 1 │ │ Span 2 │ │ Span 3 │ ...       │  │
│  │  └────────┘ └────────┘ └────────┘           │  │
│  └─────────────────────────────────────────────┘  │
│  ┌─────────────────────────────────────────────┐  │
│  │              STACKS                          │  │
│  │  (per-goroutine, growable)                   │  │
│  └─────────────────────────────────────────────┘  │
│  ┌─────────────────────────────────────────────┐  │
│  │              GLOBALS / BSS                   │  │
│  │  (package-level variables)                   │  │
│  └─────────────────────────────────────────────┘  │
│  ┌─────────────────────────────────────────────┐  │
│  │              TEXT (Code)                     │  │
│  │  (compiled machine code)                     │  │
│  └─────────────────────────────────────────────┘  │
└───────────────────────────────────────────────────┘

Allocation Hierarchy

Allocation Request
      │
      ▼
┌──────────────┐
│  tiny alloc  │ ← < 16 bytes, no pointers
│  (bump ptr)  │
└──────┬───────┘
       │ miss
       ▼
┌──────────────┐
│   mcache     │ ← Per-P cache (no locking!)
│  (per-P)     │   Contains spans for each size class
└──────┬───────┘
       │ miss
       ▼
┌──────────────┐
│   mcentral   │ ← Global cache per size class (with locking)
│  (per-size)  │
└──────┬───────┘
       │ miss
       ▼
┌──────────────┐
│    mheap     │ ← OS se pages request karta hai
│  (global)    │   mmap() / VirtualAlloc()
└──────────────┘

Size Classes

Go allocator ~67 size classes use karta hai:

Class  Size    Objects/Page  Waste(%)
  1       8       512         0.00
  2      16       256         0.00
  3      24       170         0.29
  4      32       128         0.00
  5      48        85         0.29
  6      64        64         0.00
  7      80        51         0.39
  8      96        42         2.08
  ...
 67    32768         1         0.00

// 32KB se bada → directly from heap (large allocation)

Size Class ka kaam:

Request: 20 bytes
    ↓
Round up to nearest size class: 24 bytes (class 3)
    ↓
Allocate 24 bytes from span of class 3
    ↓
Waste: 4 bytes (20/24 = ~17% waste for this request)
    ↓
Overall waste controlled to max 12.5%

Generated by src/runtime/_mkmalloc/mksizeclasses.go


mallocgc — Core Allocation Function

// src/runtime/malloc.go
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
    // 1. Tiny allocation (< 16 bytes, no pointers)
    //    → bump pointer in tiny allocator
    
    // 2. Small allocation (16 bytes - 32 KB)
    //    → find appropriate size class
    //    → allocate from mcache span
    
    // 3. Large allocation (> 32 KB)
    //    → allocate directly from heap
    //    → gets its own span
    
    // 4. If GC needed → trigger GC
    
    // 5. Return pointer
}

Tiny Allocator (< 16 bytes)

// Very small objects without pointers (like small ints, bools)
// Combine multiple tiny objects into one 16-byte block
// Example:
var a int8  // 1 byte  ┐
var b int8  // 1 byte  ├─ all in same 16-byte block!
var c int8  // 1 byte  ┘

Garbage Collector — Tri-color Mark & Sweep

GC Algorithm

┌─────────────────────────────────────────────┐
│           Tri-Color Mark & Sweep            │
├─────────────────────────────────────────────┤
│                                             │
│  WHITE = unmarked (potentially garbage)     │
│  GREY  = marked, children not scanned yet   │
│  BLACK = marked, all children scanned       │
│                                             │
│  1. Start: all objects WHITE                │
│  2. Mark roots → GREY (stacks, globals)     │
│  3. Pick GREY → scan children → BLACK       │
│  4. Repeat until no GREY left               │
│  5. Sweep: all WHITE objects = garbage!     │
│                                             │
└─────────────────────────────────────────────┘

GC Phases

Program Running
    │
    ▼
┌────────────────────┐
│  1. Mark Setup     │ ← STW (Stop-The-World) — very brief
│     (enable write  │    ~10-30 microseconds
│      barrier)      │
└────────┬───────────┘
         ▼
┌────────────────────┐
│  2. Concurrent     │ ← Goroutines AND GC run together!
│     Marking        │    25% CPU for GC by default
│     (scan objects) │
└────────┬───────────┘
         ▼
┌────────────────────┐
│  3. Mark           │ ← STW — drain remaining work
│     Termination    │    very brief
└────────┬───────────┘
         ▼
┌────────────────────┐
│  4. Concurrent     │ ← Goroutines AND sweeping together
│     Sweeping       │    Free unmarked objects
└────────────────────┘

Write Barrier

// GC concurrent hai — goroutines pointers modify kar sakti hain
// Write barrier ensures GC misses nothing
 
// Pseudo-code:
func writeBarrier(slot *unsafe.Pointer, new unsafe.Pointer) {
    // Record the old value (shade it grey)
    shade(*slot)
    // Write the new value
    *slot = new
}
 
// Ye automatically compiler insert karta hai jab GC active ho

GOGC — GC Tuning

import "runtime/debug"
 
// GOGC = target heap growth percentage before next GC
// Default: 100 (heap doubles before GC triggers)
 
// GOGC=100 (default):
// Live heap = 100MB → GC triggers at ~200MB
// GOGC=50:
// Live heap = 100MB → GC triggers at ~150MB (more frequent)
// GOGC=200:
// Live heap = 100MB → GC triggers at ~300MB (less frequent)
// GOGC=off:
// GC disabled! (DANGEROUS for long-running programs)
 
debug.SetGCPercent(50)  // programmatically set

GOMEMLIMIT — Soft Memory Limit (Go 1.19+)

// Better than GOGC for most production use
debug.SetMemoryLimit(1 << 30)  // 1 GiB soft limit
 
// GC becomes more aggressive as heap approaches limit
// But won't OOM-kill your process (it's a "soft" limit)
GOMEMLIMIT=512MiB ./myapp
GOMEMLIMIT=2GiB ./myapp

GC Trace — Debugging GC

GODEBUG=gctrace=1 ./myapp
 
# Output example:
# gc 1 @0.004s 1%: 0.012+0.26+0.010 ms clock, 0.049+0.10/0.25/0+0.041 ms cpu, 4->4->0 MB, 5 MB goal, 4 P
#
# Breakdown:
# gc 1          → GC cycle number
# @0.004s       → time since program start
# 1%            → % of time spent in GC
# 0.012+0.26+0.010 ms clock → STW1 + concurrent + STW2
# 4->4->0 MB    → heap before → heap after → live heap
# 5 MB goal     → target heap size
# 4 P           → number of processors used

Stack vs Heap — Escape Analysis Decides

// STACK allocation (fast, automatic cleanup)
func stackAlloc() int {
    x := 42        // x stays on stack — no GC needed!
    return x
}
 
// HEAP allocation (slower, needs GC)
func heapAlloc() *int {
    x := 42        // x ESCAPES to heap — pointer returned
    return &x      // GC will clean this up later
}
 
// Check with:
// go build -gcflags="-m" .

Escape Analysis Rules (simplified):

Stack (fast):
  ✓ Local variables that don't escape function
  ✓ Small fixed-size allocations
  ✓ Variables whose lifetime is clearly bounded

Heap (needs GC):
  ✗ Pointers returned from functions
  ✗ Variables stored in interfaces
  ✗ Variables captured by closures that outlive function
  ✗ Large allocations
  ✗ Variables whose size is not known at compile time

Memory Statistics

import "runtime"
 
var m runtime.MemStats
runtime.ReadMemStats(&m)
 
fmt.Printf("Alloc       = %v MB\n", m.Alloc/1024/1024)      // current heap
fmt.Printf("TotalAlloc  = %v MB\n", m.TotalAlloc/1024/1024)  // cumulative
fmt.Printf("Sys         = %v MB\n", m.Sys/1024/1024)         // OS se liya
fmt.Printf("NumGC       = %v\n", m.NumGC)                    // GC cycles
fmt.Printf("HeapObjects = %v\n", m.HeapObjects)              // live objects
fmt.Printf("GCCPUFrac   = %v%%\n", m.GCCPUFraction*100)     // GC CPU usage

runtime/metrics — Modern Metrics API (Go 1.16+)

import "runtime/metrics"
 
// Available metrics discover karo
descs := metrics.All()
for _, d := range descs {
    fmt.Printf("%s: %s\n", d.Name, d.Description)
}
 
// Specific metrics read karo
samples := []metrics.Sample{
    {Name: "/memory/classes/heap/free:bytes"},
    {Name: "/gc/cycles/automatic:gc-cycles"},
    {Name: "/sched/goroutines:goroutines"},
}
metrics.Read(samples)
 
for _, s := range samples {
    fmt.Printf("%s = %v\n", s.Name, s.Value)
}

Important Metric Categories:

CategoryExample MetricPurpose
Memory/memory/classes/heap/free:bytesFree heap memory
GC/gc/cycles/automatic:gc-cyclesAuto GC cycle count
Scheduler/sched/goroutines:goroutinesActive goroutines
CPU/cpu/classes/gc/total:cpu-secondsCPU time in GC
Sync/sync/mutex/wait/total:secondsMutex wait time

Finalizers

import "runtime"
 
type Resource struct {
    handle int
}
 
func NewResource() *Resource {
    r := &Resource{handle: openHandle()}
    
    // GC jab Resource collect karega, pehle ye function chalega
    runtime.SetFinalizer(r, func(r *Resource) {
        closeHandle(r.handle)
        fmt.Println("Resource cleaned up!")
    })
    
    return r
}
 
// WARNING: Finalizers pe depend mat karo!
// - Order guaranteed nahi hai
// - Timing guaranteed nahi hai
// - Performance impact hai
// Prefer: explicit Close() method with defer

Related