In the previous post, we explored how Go allocates variables to the stack or heap. Now, let’s look at how Go reclaims memory that is no longer in use. Our memory is finite, so let’s dive into the garbage collector.
What is a garbage collector?
The garbage collector (GC) is a memory-management tool. It helps maintain efficient performance and prevents many memory leaks by reclaiming memory that a program no longer uses, so that space can be reused.
Garbage collection in mainstream high-level programming languages generally falls into two categories:
- Manual: In languages such as C, programmers are responsible for releasing the memory they allocate.
- Automatic: In languages such as Java, Python, and Go, the memory-management system reclaims memory while the program is running.
The evolution of Go’s GC
Since its first stable release, Go has continued to improve its garbage collector. Here are some of the major changes along the way.
Go 1.0 to 1.4: The stop-the-world era
- Written in C: The runtime, including memory allocation and garbage collection, was implemented in C.
- Stop-the-world: Garbage collection was synchronous. When a collection cycle began, the program paused while the GC scanned and reclaimed memory.
- Conservative: Unlike modern Go, which tracks pointers precisely, early Go used a conservative collector. It could not always tell whether a value on the stack or heap was a pointer or an integer that happened to resemble a memory address.
- Mark and sweep: The collector traversed the object graph from its roots, marking reachable objects as live, and then swept through the heap to reclaim unmarked allocations.
- Non-generational: Objects were not grouped into generations by age, as they are in some other runtimes, such as the JVM. Each GC cycle evaluated the entire heap.
Go 1.5: A big jump toward concurrency
- Tri-color mark-and-sweep: The GC adopted a tri-color marking algorithm. Objects are categorized as white (unvisited), gray (visited but not scanned), or black (visited and scanned).
- Concurrent collection: The GC could scan and mark live memory while application code was running.
- Lower latency: Instead of stopping the world for the entire cycle, Go 1.5 briefly stopped it twice: once to enable the write barrier and once to finish the marking phase.
- Write barrier: Go 1.5 introduced a write barrier: a small piece of compiler-generated code that intercepts pointer assignments during GC so live objects are not missed.
- Pacing and GC assists: To keep applications from allocating memory faster than the concurrent GC could process it, Go 1.5 introduced GC pacing. If a goroutine allocated too quickly, the runtime could make it assist with marking.
Go 1.19
- Soft memory limit: Go 1.19 introduced the
GOMEMLIMITenvironment variable, which lets developers set a soft memory limit for the runtime. - Dynamic pacing: As memory usage approaches
GOMEMLIMIT, the GC can adjust its pacing and run more frequently. - Protection against a death spiral: The runtime limits the GC’s CPU use to avoid spending all available CPU time on collection, even if that means exceeding the soft memory limit.
- Short stop-the-world pauses: Ongoing refinements have reduced stop-the-world pauses to very short durations, often below a millisecond.
Go 1.25
- Green Tea’s block-based architecture: Green Tea makes the GC more memory-aware by working with 8 KiB memory blocks. Processing spans of small objects in batches can improve CPU cache efficiency and spatial locality.
- Reduced GC overhead: By reducing pointer chasing, the Green Tea collector can lower GC CPU overhead in workloads with many objects, such as microservices and JSON parsers.
- CPU scalability and locality: Organizing GC work around memory blocks can help it scale across CPU cores and reduce contention on large servers.
- A foundation for SIMD acceleration: A block-based span model may enable vectorized operations that scan and mark multiple blocks at once.
Conclusion
Go’s garbage collector has evolved from a collector that paused the program for much of its work into one that can do most of its work concurrently. Improvements to pacing, memory limits, and the Green Tea collector continue to reduce the cost of managing memory. Understanding that evolution helps explain why Go can reclaim unused memory automatically while still aiming to keep application performance responsive.