Go memory management
Basic keywords
- 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.
- 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 likeGOMEMLIMIT
to achieve this. - 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.
- 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:
|
|
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:
- 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.
- Goroutines : Already discussed above.
- 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:
|
|
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 globalmheap
object.mspan
: Structure of storage which handles the pages of memory inmheap
. It’s a double-linked list which holds all the page related info and data itself. Eachmspan
size can vary from 8KB to 32 KB. Also each span of type exists twice so thatscan
andnoscan
classes can be differentiated and GC will just lookscan
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 twospanlist
:- Empty: Those which are completely allocated (
c->empty
) - 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 free
spanis left the more are requested from
mheap`
- Empty: Those which are completely allocated (
mcache
: This a cache memory which is local to the logical processor and this contains of pointers to themcentral
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 multiplemspan's
directly instead, it contains free lists that point to spans frommcentral
.
Relationship between mheap, mcentral, mcache
:
|
|
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.
- Yes,
- What is stack-allocated
noscan
object?arr := [100]int{} // Stack-allocated, not on heap
. Stack allocatednoscan
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 thismcache
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 frommcentral
, if that doesn’t have it, it gets frommheap
, if that also doesn’t have it we can request from OS usingmmap
.
- Both tiny and small allocations are handled within
mcache
contains just pointer to the mspan inmcentral
ormheap
?- Yes,
mcache
contains pointers tomspan
, which are managed bymcentral
andmheap
. It does not store raw memory but acts as a cache for fast allocation. - Actual object data is stored in
mspan
, notmcache
. - The 2 MB limit is for the total data allocated in
mspan's
withinmcache
. - The 2 MB per
mcache
is the sum of all object data stored inmspan's
it holds reference to.
- Yes,
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