In runtime.MemStats, how do HeapAlloc, HeapInuse, HeapIdle and HeapReleased differ?
answer
- four fields, four different questions
- objects, full spans, empty spans, given back
- idle is still owned by the runtime
- released has actually left the process
basics
~20 sHeapAlloc counts bytes in heap objects, including garbage not yet swept. HeapInuse counts bytes in spans holding at least one object, HeapIdle bytes in spans holding none, and HeapReleased the idle part already given back to the operating system.
solid answer
~50 sThey sit at different levels. `HeapAlloc` is object-level: bytes in allocated heap objects, and it includes garbage the sweeper has not reached yet, so it is *not* a clean live-heap figure. `HeapInuse` and `HeapIdle` are span-level: the runtime manages memory in spans, and a span counts as in-use if it holds at least one object and idle if it holds none. `HeapReleased` is the slice of idle memory whose pages have actually been returned to the operating system. Two differences carry the diagnostic weight: `HeapInuse - HeapAlloc` is space wasted inside spans that still hold something, and `HeapIdle - HeapReleased` is memory the runtime has freed internally but is deliberately holding onto so it can grow the heap again without asking the kernel. `Sys` sits above all of it as the total obtained from the operating system.
code
go · 11 linesvar m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("objects %d MB\n", m.HeapAlloc>>20)
fmt.Printf("in-use %d MB\n", m.HeapInuse>>20)
fmt.Printf("idle %d MB\n", m.HeapIdle>>20)
fmt.Printf("released %d MB\n", m.HeapReleased>>20)
fmt.Printf("from OS %d MB\n", m.Sys>>20)
// held by the runtime, unused by the program:
fmt.Printf("retained %d MB\n", (m.HeapIdle-m.HeapReleased)>>20)go deeper
Know that runtime.ReadMemStats fills a struct you pass by pointer, and that HeapAlloc is about objects while the other three are about spans of memory. Do not quote a single field as the process's memory use.
Explain the object-versus-span split and give the two meaningful differences: in-use minus allocated is fragmentation, idle minus released is memory the runtime deliberately holds. That is the mechanical account expected here.
Show you use these fields to settle arguments: which one you compare against resident memory, why one sample proves nothing, and that ReadMemStats stops the world so it goes on a scrape interval rather than a request path.
Decide which of these become standing service metrics and which stay incident-only tools, and make sure the dashboard nobody will re-read at 3am shows the differences that mean something rather than a single raw field.
## Getting the numbers `runtime.ReadMemStats(&m)` fills a `runtime.MemStats` struct with the allocator's current accounting. It takes a pointer to a struct you own rather than returning one, and it briefly stops the world so the snapshot is internally consistent — which is why it belongs on a periodic path, not in a hot loop or on every request. The struct has dozens of fields. Four of them describe the heap, and confusing them is the root of most "is this a leak" arguments. ## Two different levels of accounting The key to keeping them straight is that Go's allocator works at two levels. **Objects** are what your code allocates. **Spans** are runs of pages the runtime carves objects out of. `HeapAlloc` counts objects; `HeapInuse`, `HeapIdle` and `HeapReleased` count span memory. **`HeapAlloc`** is bytes of allocated heap objects. Two things about it surprise people. First, it rises as you allocate and falls only when the sweeper actually frees an object — so it includes objects that are already unreachable but not yet swept. Reading it at an arbitrary instant therefore overstates the live heap, sometimes considerably, and reading it right after a cycle understates the peak. Second, it is object bytes only: no goroutine stacks, no runtime metadata. `MemStats.Alloc` holds the same value. **`HeapInuse`** is bytes in spans that hold at least one object. Because a span is claimed whole for one size class, a span holding a single small object counts entirely as in-use. `HeapInuse - HeapAlloc` is therefore the free slots inside in-use spans: internal fragmentation. Ordinary programs have some; a program that allocated a huge number of objects of one size and then freed nearly all of them can have a lot. **`HeapIdle`** is bytes in spans that hold no objects at all. This memory is free from your program's point of view and available for the runtime to reuse, but the runtime still owns it. It is the field people forget, and forgetting it is how a healthy process gets mislabelled as leaking. **`HeapReleased`** is the part of that idle memory whose physical pages have actually been given back to the operating system. So `HeapIdle - HeapReleased` is the memory the runtime is holding, unused, on purpose: keeping it means the next growth spurt needs no syscall and no new mapping. ## How they fit together `HeapInuse + HeapIdle` accounts for the heap span memory the runtime holds. Above the heap, `HeapSys` is the heap memory obtained from the operating system — reserved address space, so it behaves like a high-water mark. Above that, `Sys` is the total obtained from the operating system across everything: heap, goroutine stacks (`StackSys`), collector metadata (`GCSys`), and the rest. `Sys` is the field to compare against the process's resident set size, and resident memory normally sits at or below it. The ordering that follows is worth memorising: `HeapAlloc <= HeapInuse <= HeapInuse + HeapIdle <= HeapSys <= Sys`. If someone quotes `HeapAlloc` and the RSS in the same breath and calls the difference a leak, that chain is the answer. ## Which field answers which question - *How much memory do my objects occupy right now?* — `HeapAlloc`, with the caveat that unswept garbage is included. - *Am I fragmenting?* — `HeapInuse - HeapAlloc`. - *Is the runtime sitting on memory it is not using?* — `HeapIdle - HeapReleased`. - *How much has this process taken from the kernel in total?* — `Sys`. - *How many objects, not bytes, am I holding?* — `HeapObjects`, useful when the suspicion is many small objects rather than a few large ones. ## Other fields worth knowing exist `TotalAlloc` is cumulative bytes ever allocated for heap objects; it never decreases, so its slope is an allocation-rate measure rather than a memory measure. `NumGC` counts completed cycles. `GCCPUFraction` is the same cumulative collector CPU share that appears as the percentage in a gctrace line. `NextGC` is the heap target for the coming cycle. `StackInuse` is goroutine-stack memory in use, which is where a goroutine leak shows up rather than in any heap field. ## The practical failure mode The reason to hold these four apart is that a single one of them, read alone, supports the wrong conclusion in both directions. A big `HeapAlloc` right before a cycle is not a leak. A small `HeapAlloc` next to a large resident set is not proof that everything is fine either — you have to look at `HeapIdle`, `HeapReleased` and `Sys` to know whether the memory is genuinely free inside the runtime or genuinely gone.
- Why can runtime.MemStats.HeapAlloc be larger than the memory your program can still reach?Because it counts allocated heap objects, and an object stays counted until the sweeper frees it. Between the end of marking and the completion of sweeping, plenty of unreachable objects are still in the total. That is why a single HeapAlloc reading is a poor live-heap estimate, and why the live-heap figure a completed collection reports is the cleaner number for retention questions.
- Which MemStats field is the right one to compare against the process's resident memory?`Sys` — the total obtained from the operating system across the heap, goroutine stacks and runtime metadata. Resident memory normally sits at or below it. Comparing RSS against `HeapAlloc` instead ignores idle spans, stacks and collector structures, and the gap that comparison invents is what gets healthy processes reported as leaking.
- What does calling runtime.ReadMemStats cost?It briefly stops the world so the accounting it copies out is internally consistent. The pause is short, but it is a real pause on every call, so it belongs on a periodic path — once a scrape interval, say — and never inside a request handler or a hot loop where its cost scales with how often the code runs.
- Where would a goroutine leak show up in runtime.MemStats?Not in the heap fields, at least not first. Each goroutine has a stack, so `StackInuse` and `StackSys` grow with the goroutine count. A process whose resident memory climbs while `HeapAlloc` stays flat and `StackInuse` rises is describing goroutines that never finish rather than objects that are never freed.
HeapInuse is shelving with something on it, HeapIdle is empty shelving you are still renting, and HeapReleased is the shelving you actually handed back to the landlord. HeapAlloc counts the boxes, including the ones already marked for disposal but not yet carried out.
saying these in an interview costs you the question
- Calls HeapAlloc the live heap with no mention of unswept garbage
- Assumes HeapIdle memory has already gone back to the OS
- Compares MemStats.Alloc directly against the process RSS
- Thinks HeapInuse counts only the bytes objects occupy
- Calls ReadMemStats on every request as if it were free