Prashant Shubham

Prashant Shubham

Computers are all I want to know about

10 Feb 2025

Go memory management


Basic keywords

  1. Stack : Stack is the place where static, global variables etc. of the application live. So basically whenever you’ve an application and it has data structures with pre-allocated memory that goes to stack, similarly stack is responsible for functions callback and trace etc.
  2. Heap : Heap is the area where a program/applications dynamic memory is allocated. Whenever we’ve places in memory where we ask the OS for lending memory to us that’s the case where heap actually comes into picture. Every application by default has some memory allocated when they’re executed. OS will not allow an application to keep expanding and consuming more memory so that other applications are affected thus it gives them a fix amount of memory. We can expand this memory if needed like in Java we can use -Xmx flag to do this. In golang we can use runtime flags like GOMEMLIMIT to achieve this.
  3. Garbage collector : Garbage collector is one of the parts of programming language which allows the language to collect the object which are not in use anymore or doesn’t have any reference to it. There are different sorts of GC’s like “Mark & Sweep”, “Reference Counting” etc.
  4. Goroutine : Go’s equivalent to threads, only different is these are not OS threads but virtual threads which can be created as much as required(obviously within the memory limit of application) and are scheduled accordingly by pinning them to the OS/Platform threads. Each goroutine has it’s 1 stack per goroutine while sharing the applications heap memory across multiple goroutines.

Go memory world

In golang we’ve a different sort of approach towards memory allocation compared to other languages like Java etc. It’s different in multiple ways like:

  • Go has very low latency GC pause times compared to Java where it can have long pauses.
  • Go has aggressive/frequent escape analysis compared to Java
  • Memory regions are different in golang like mcache used for tiny allocations thus making allocations faster.

One important aspect of golang is scan and noscan classes, scan classes are the one which do have a reference embedded in them while noscan classes are the one which have no references/pointers to other objects. e.g:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// SCAN class
type Node struct {
    next *Node // Pointer field (requires GC tracking)
    value int
}

// NOSCAN class
type Data struct {
    id int64
    count int32
}

For noscan class the GC doesn’t need to scan hence reducing the memory overhead, while in case of scan class the GC must scan these objects to track references for cleanup.

Now let’s look at Go’s internal memory structure, it consists of 3 major things:

  1. Go scheduler : This in itself will require a detailed blog, but for now we can understand that scheduler is the one which helps with gorutine management and scheduling each of them and pinning them with OS so they can get their task done.
  2. Goroutines : Already discussed above.
  3. Logical Processors : Logical processor is basically the CPU on which the goroutines execute.

The memory structure of golang starts at high level with “Resident set” which is nothing but the memory allotted to the application. Then we’ve mheap, mcentral, mspan, mcache which are the defined blocks of memories in heap. Below is a diagram of how all these correlate together:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
+---------------------------+
|      Resident Set         |  (Overall process memory)
|  +---------------------+  |
|  |       mheap         |  |  (Global heap memory)
|  | +---------------+   |  |
|  | |   mcentral   |    |  |  (Central free list)
|  | | +---------+  |    |  |
|  | | |  mspan  |  |    |  |  (Unit of allocation)
|  | | +---------+  |    |  |
|  | +---------------+   |  |
|  +---------------------+  |
|                           |
|  +---------------------+  |
|  |      mcache         |  |  (Per-processor cache for fast allocs)
|  | +---------------+   |  |
|  | |  mspan ptrs   |   |  |  (Holds pointers to free spans)
|  | +---------------+   |  |
|  +---------------------+  |
|                         | 
|         |    |            |  (Goroutines allocate from mcache)
|  +--------+  +--------+   |
|  |  G1    |  |  G2    |   |  (Goroutines running on processors)
|  +--------+  +--------+   |
|      |          |         |
|  +--------+  +--------+   |
|  | Stack  |  | Stack  |   |  (Each goroutine has its own stack)
|  +--------+  +--------+   |
+---------------------------+

Let’s delve into these concepts of internal memory parts:

  • mheap : This is where Go stores dynamic data (any data for which size cannot be calculated at compile time). This is the biggest block of memory and this is where GC takes place. The resident set is divided into pages of 8KB each and is managed by one global mheap object.
  • mspan : Structure of storage which handles the pages of memory in mheap. It’s a double-linked list which holds all the page related info and data itself. Each mspan size can vary from 8KB to 32 KB. Also each span of type exists twice so that scan and noscan classes can be differentiated and GC will just look scan classes for live objects.
  • mcentral : It holds lists of spans of a specific size class. Some spans are completely full (and thus not available for allocation), some are partially full, and some are completely empty. mcentral consists of two spanlist:
    1. Empty: Those which are completely allocated (c->empty)
    2. Non-empty: Those with free objects (c->nonempty) Yes the naming is confusing but we can think of it in way that empty means it doesn't have any allocation to offer. The empty and nonempty span lists are used to quickly find spans with free objects. When spanis freed it moves from empty to non-empty and when allocated it does vice-versa. When no freespanis left the more are requested frommheap`
  • mcache : This a cache memory which is local to the logical processor and this contains of pointers to the mcentral this helps in faster tiny allocations. This also allows goroutines to get small memory allocated without locks as any logical processor has only one goroutine at one point of time. mcache itself does not contain multiple mspan's directly instead, it contains free lists that point to spans from mcentral.

Relationship between mheap, mcentral, mcache:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
+------------------------+
|         P (Processor)  |
+------------------------+
          
          
+----------------------+
|       mcache        |  (Per-Processor Cache)
|----------------------|
|  Free lists (ptrs)  |──► [ mspan (for 16B) ]──► Object Memory
|  for each size      |──► [ mspan (for 32B) ]──► Object Memory
|  class              |──► [ mspan (for 64B) ]──► Object Memory
+----------------------+
          
          
+----------------------+
|       mcentral      |  (Global Pool of Spans)
|----------------------|
|  Full spans list    |──► [ mspan (allocated) ]
|  Empty spans list   |──► [ mspan (available) ]
+----------------------+
          
          
+----------------------+
|       mheap         |  (Memory from OS)
|----------------------|
|  Large objects      |  (>32KB allocations)
|  Allocated spans    |──► New spans given to `mcentral`
|  Free spans         |
+----------------------+

So this is a high level overview of how memory structure of golang looks like, I’ll write down few questions which I thought while reading this below these might be dumb ones but don’t judge me for that!!!

Questions

  • noscan objects would also consume memory? Then when does the GC collect it?
    • Yes, noscan objects still consume memory, but since they don’t contain pointers, Go’s garbage collector (GC) doesn’t need to scan them to track references. However, they are still managed by GC and will be freed when they become unreachable.
  • What is stack-allocated noscan object?
    • arr := [100]int{} // Stack-allocated, not on heap. Stack allocated noscan objects disappears when function exits (no GC needed).
  • Explain how do we track reachability to an object?
    • Reachability refers to whether an object can still be accessed by running code. If an object is unreachable, it is considered garbage and eligible for collection. Go tracks reachability using a Tracing Garbage Collector (GC) based on the Mark-and-Sweep algorithm. This requires a whole new article in itself 😅.
  • Tiny and small allocations are both done on the mcache allocator? What’s the size of this mcache allocator attached with the processor for go-routine? And when it runs out of that memory what happens for tiny/small allocations?
    • Both tiny and small allocations are handled within mcache, which is a per-processor cache for memory allocations in Go.
    • Each mcache has a fixed-size allocation cache of 2 MB per processor.
    • When an mcache exhausts its cached memory it refills from mcentral, if that doesn’t have it, it gets from mheap, if that also doesn’t have it we can request from OS using mmap.
  • mcache contains just pointer to the mspan in mcentral or mheap?
    • Yes, mcache contains pointers to mspan, which are managed by mcentral and mheap. It does not store raw memory but acts as a cache for fast allocation.
    • Actual object data is stored in mspan, not mcache.
    • The 2 MB limit is for the total data allocated in mspan's within mcache.
    • The 2 MB per mcache is the sum of all object data stored in mspan's it holds reference to.

Hope you find this article useful, thanks for reading!!! Please report if you find any issues in the article you can reach out to me on linkedin or my mail