Go

Why is the process RSS much larger than the heap in use? How does Go return memory to the OS?

Question 343HardGo 1.22 to 1.25

RSS includes more than live objects:

  • Garbage not yet collected. With GOGC=100 the heap may reach about twice the live size.
  • Idle spans that were freed but not yet returned to the OS.
  • Goroutine stacks, GC metadata, fragmentation within size classes, and memory allocated by cgo.

A background scavenger returns idle pages to the OS with madvise. On Linux the default is MADV_DONTNEED (since Go 1.16); earlier versions used MADV_FREE, whose pages stayed in RSS until the kernel was under pressure. The scavenger works gradually, aiming to keep retained memory close to the heap goal, and becomes more aggressive near GOMEMLIMIT. debug.FreeOSMemory() forces a GC and returns as much as possible, which helps after a one-off spike.

import "runtime/metrics"

func report() {
    s := []metrics.Sample{
        {Name: "/memory/classes/heap/objects:bytes"},
        {Name: "/memory/classes/heap/released:bytes"},
        {Name: "/memory/classes/heap/free:bytes"},
        {Name: "/memory/classes/total:bytes"},
    }
    metrics.Read(s)
    for _, m := range s {
        fmt.Printf("%-45s %d\n", m.Name, m.Value.Uint64())
    }
}

Tip: prefer runtime/metrics over runtime.ReadMemStats, which historically needed a stop-the-world. HeapReleased is memory Go has already handed back, so it no longer counts toward RSS.

More on Memory, GC & Runtime Internals

All 38 Memory, GC & Runtime Internals questions