Garbage collection (GC) is one of the most critical components of any modern programming language runtime. It decides how and when memory is reclaimed, directly impacting latency, throughput, and the overall responsiveness of applications.
Go has always prioritized simplicity and developer productivity, and its garbage collector plays a major role in that story. Unlike languages such as C and C++ that leave memory management to the programmer, Go ships with a sophisticated GC designed to keep latency low while scaling to multi-core systems.
In Go 1.25, the garbage collector underwent significant changes. A new algorithm, internally called Green Tea, replaced core parts of the tri-color mark-and-sweep approach that Go had used since its early releases. This shift represents more than just an implementation detail - it's a major step in Go's long-term strategy to provide predictable, low-latency GC for high-concurrency applications.
In this article, we'll take a step back and look at the evolution of garbage collection strategies, leading up to Go's current approach. We'll cover three major milestones:
But this won't just be theory. To really understand the differences, we'll:
By the end, you'll not only understand what changed in Go 1.25, but also gain deeper insight into the trade-offs behind GC design - knowledge that's valuable far beyond Go itself.
The full code for the toy implementations, benchmarks, and demos is available in this repository: https://github.com/gkoos/article-gc
Before we can talk about garbage collection algorithms, we need a mental model of the heap - the area of memory where dynamically allocated objects live.
Go's memory management system is highly optimized and far from trivial. Some of the key aspects include:
This system is fast, scalable, and concurrency-friendly - but it's also very complex. Explaining it in full would require an article series on its own.
For the purpose of this article, we don't need to replicate every detail of the Go runtime. Instead, we want a simple but illustrative model of the heap that will let us:
This simplified heap won't capture performance optimizations like spans, arenas, or write barriers exactly as in Go, but it will make the algorithms much easier to explain and compare.
We'll represent the heap as a collection of Object structs, each with:
Here's a basic sketch:
type Object struct { ID int Refs []*Object // references to other objects Marked bool // used in tri-color / Green Tea RefCount int // used in reference counting }
We'll also maintain a global slice of all objects to represent the heap:
var heap []*Object
And we'll define a simple root set - objects that are "always reachable" (like global variables or stack roots in a real program):
var roots []*Object
This toy model gives us just enough structure to experiment with different garbage collection algorithms, without drowning in the details of the production Go runtime.
\ Now that we have a toy model of the heap, let's look at the algorithms that have been used for garbage collection.We'll cover three stages in the evolution of GC: Reference Counting, Tri-Color Mark-and-Sweep, and Green Tea.
Reference counting (RC) is one of the simplest forms of garbage collection.Every object keeps a counter of how many references point to it.When a new reference is created, the counter increases.When a reference is removed, the counter decreases.When the counter reaches zero, the object can be freed immediately.
Advantages:
Limitations:
Historical note: Reference counting was popular in early systems due to its simplicity, and it's still a cornerstone of some languages (Python, Swift). But its limitations motivated more sophisticated algorithms.
The tri-color abstraction is the backbone of most modern garbage collectors.Objects are divided into three sets during a collection cycle:
The algorithm works roughly like this:
Advantages:
Limitations:
Historical note: Dijkstra's 1978 "on-the-fly" GC introduced the idea of tri-color marking, and it's influenced JVM, .NET, and Go.
Go 1.25 introduced a major change with Green Tea, an algorithm designed to scale better across many cores and reduce coordination costs.
Instead of thinking in terms of objects and colors, Green Tea shifts the perspective to spans - contiguous chunks of memory that contain multiple objects of the same size class.
Key ideas:
Advantages:
Limitations:
Historical note: Green Tea builds on decades of research into parallel and concurrent GCs. Similar span- and region-based strategies appear in JVM G1GC and Azul's collectors, but Go's variant is tuned specifically for its concurrency model.
While our focus is on Reference Counting, Tri-Color, and Green Tea, other garbage collection strategies are worth knowing about:
These approaches influenced the design of Go's GC, but Go has deliberately chosen simplicity and predictability over the complexity of full generational or concurrent copying collectors.
With the concepts in place, let's implement our toy garbage collectors.We'll use the simplified Object and heap structures we defined earlier, and implement each algorithm in turn: Reference Counting, Tri-Color Mark-and-Sweep, and Green Tea.
We already defined our Object struct and global heap and roots. Now, let's implement the core functions for our toy heap:
func NewObject(id int) *Object { obj := &Object{ID: id} heap = append(heap, obj) return obj } func AddRoot(obj *Object) { roots = append(roots, obj) } func AddRef(from, to *Object) { from.Refs = append(from.Refs, to) to.RefCount++ }
These helper functions allow us to create objects, define roots, and establish references between objects.
Reference counting updates counters when references are added or removed. Collection is immediate: when an object's count drops to zero, we recursively free it.
func RemoveRef(from, to *Object) { // remove reference from "from" to "to" newRefs := []*Object{} for _, r := range from.Refs { if r != to { newRefs = append(newRefs, r) } } from.Refs = newRefs // decrement counter to.RefCount-- if to.RefCount == 0 { freeObject(to) } } func freeObject(obj *Object) { // recursively free children for _, r := range obj.Refs { r.RefCount-- if r.RefCount == 0 { freeObject(r) } } // remove from heap newHeap := []*Object{} for _, h := range heap { if h != obj { newHeap = append(newHeap, h) } } heap = newHeap fmt.Printf("Freed object %d\n", obj.ID) }
Description:
RefCount).Here's a simple tri-color collector. We'll use Marked as the "color": false = white, true = black. The gray set is represented by a queue.
func TriColorGC() { // 1. Mark phase worklist := []*Object{} // gray set for _, root := range roots { if !root.Marked { root.Marked = true worklist = append(worklist, root) } } for len(worklist) > 0 { obj := worklist[0] worklist = worklist[1:] for _, r := range obj.Refs { if !r.Marked { r.Marked = true worklist = append(worklist, r) } } } // 2. Sweep phase newHeap := []*Object{} for _, obj := range heap { if obj.Marked { obj.Marked = false // reset for next GC newHeap = append(newHeap, obj) } else { fmt.Printf("Swept object %d\n", obj.ID) } } heap = newHeap }
Description:
Marked to represent object color.worklist is the gray set: objects reachable but not yet scanned.How to see the pause:
TriColorGC() runs synchronously: the main allocation loop cannot proceed while it executes.Our toy model won't fully replicate Go 1.25's span-based GC, but we can simulate the key idea: work distribution at the span level.
Here we treat each "span" as a batch of objects and mark them together.
const spanSize = 2 // just for demonstration func GreenTeaGC() { // divide heap into spans spans := [][]*Object{} for i := 0; i < len(heap); i += spanSize { end := i + spanSize if end > len(heap) { end = len(heap) } spans = append(spans, heap[i:end]) } // mark reachable objects marked := map[*Object]bool{} worklist := roots for len(worklist) > 0 { obj := worklist[0] worklist = worklist[1:] if marked[obj] { continue } marked[obj] = true for _, r := range obj.Refs { worklist = append(worklist, r) } } // sweep whole spans newHeap := []*Object{} for _, span := range spans { keepSpan := false for _, obj := range span { if marked[obj] { keepSpan = true break } } if keepSpan { for _, obj := range span { if marked[obj] { newHeap = append(newHeap, obj) } } } else { for _, obj := range span { fmt.Printf("GreenTea swept object %d\n", obj.ID) } } } heap = newHeap }
Description:
How to see concurrency:
To demonstrate the practical differences between Tri-Color and Green Tea, cmd/bench/main.go implements a main work completion benchmark. The goal is simple: simulate a program performing allocations while the GC runs, and measure how quickly the main routine completes.
HEAP_SIZE objects.ROOTS root objects, each pointing to a chain of SPAN_SIZE objects.MAIN_ALLOC new objects).To run the benchmark, use:
go run cmd/bench/main.go
You should see output similar to:
[TriColor] Main work completed in: 5.0025ms [GreenTea] Main work completed in: 3.8372ms
This output indicates that the main work completed faster with Green Tea, demonstrating its advantage in reducing application pause times.
Although this is a simplified model and the numbers are illustrative only, it effectively showcases the key differences between blocking and incremental GC strategies, aligning with Go's goals for low-latency garbage collection.
To make these ideas more interactive, cmd/demo/main.go serves as a playground for experimenting with our toy heap and garbage collectors.
cmd/demo/main.go) reduces blocking.You can run the demo with:
go run cmd/demo/main.go
This playground is designed for experimentation. Break it, tweak it, make cycles, expand the heap! The goal is to see the algorithms in action and build intuition about garbage collection, not just read about it.
Go's shift from a classic tri-color GC to the span-based Green Tea collector reflects a set of practical priorities:
In short, Go's move reflects the latest advancements in garbage collection research combined with the practical realities of large, high-concurrency programs. The result is a collector that scales efficiently without compromising Go's core promise: predictable, low-latency performance.
Garbage collection is more than a runtime detail - it directly affects how responsive and efficient your programs are. By exploring reference counting, tri-color mark-and-sweep, and Green Tea, we've seen:
Even if you never implement a garbage collector yourself, understanding these trade-offs gives you a sharper intuition about performance, memory behavior, and the hidden costs behind seemingly simple Go programs.
With these insights, you can better reason about allocation patterns, concurrency, and performance optimizations, and appreciate the engineering behind Go's modern garbage collector.
All our toy examples were cooked up with Go 1.23 — couldn't resist. 😄
But don't worry: with Go 1.25, the results should be very similar, so you can play around and see the differences between Tri-Color and Green Tea for yourself.


