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:

  1. Reference Counting — a simple but limited strategy that was historically popular.
  2. Tri-Color Mark-and-Sweep — the incremental algorithm that powered Go's GC until 1.25.
  3. Green Tea — the new span-based algorithm introduced in Go 1.25.

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

Heap Representation (Real Go vs. Our Simplification)

Disclaimer: The toy implementations below are for educational purposes and do not reflect all optimizations, concurrency mechanisms, or edge cases present in Go's production garbage collector.

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.

How Go Actually Manages the Heap

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.

Why We Need a Simplified Model

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.

Our Toy Heap

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.

Algorithms and Their Evolution

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

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.


Tri-Color Mark-and-Sweep

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:

  1. Start by putting all root objects in the gray set.
  2. While there are gray objects:
    • Pop one from the gray set.
    • Mark all objects it references as gray (if not already black/gray).
    • Mark the current object black.
  3. When no gray objects remain, all white objects are unreachable, so free them.

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.


Green Tea (Go 1.25+)

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.

Other Algorithms (Honorable Mentions)

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.

Implementation

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.

Common Toy Heap Code

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 Implementation

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:

Tri-Color Mark-and-Sweep Implementation

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:

How to see the pause:

Green Tea (Span-based Approximation)

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:

Benchmarking Main Work Completion

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.

What the Benchmark Does

  1. Heap Setup
    • Creates a heap of HEAP_SIZE objects.
    • Builds ROOTS root objects, each pointing to a chain of SPAN_SIZE objects.
    • This creates a mix of reachable and unreachable objects, simulating a realistic heap layout.
  2. Main Work Simulation
    • A separate goroutine simulates the application doing allocations (MAIN_ALLOC new objects).
    • This represents typical program work independent of garbage collection.
  3. Tri-Color GC (Blocking)
  1. Green Tea GC (Incremental)
    • Runs concurrently in its own goroutine.
    • Splits the heap into spans and marks reachable objects incrementally.
    • Every 100 objects marked, the GC yields (time.Sleep) to simulate cooperative concurrency.
    • Sweep is also done span by span.
    • The main routine continues allocating objects without being blocked, illustrating the reduced pause times of Green Tea.
  2. Measurement
    • The benchmark measures how long the main work goroutine takes to complete under each GC strategy.
    • This directly shows the impact of GC pauses on application responsiveness, which is the core design goal of Green Tea.

Note: Although tri-color marking is incremental in theory, our benchmark runs it as a blocking, synchronous operation to model the stop-the-world pauses that still occur in real implementations. In contrast, the Green Tea algorithm is designed to run incrementally and concurrently, so our benchmark allows the main work to continue while Green Tea GC operates in a separate goroutine, better reflecting its low-latency, non-blocking behavior.

Why This Approach Works

Running the Benchmark

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.

Playground: Experimenting with 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.

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.

Why Go Moved Toward Green Tea

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.

Conclusion

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.

A Note on Go Version

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.