Garbage Collection Guide

Purpose and Scope

This guide orients runtime readers to the major source files behind Go garbage collection and to the concepts those files expose: collection cycles, pacing, sweeping, and memory statistics. The garbage collector is not a separate tool in the distribution; it is part of the runtime package that every Go program links. For application developers, the most visible results are allocation latency, heap growth, pause times, and values returned through runtime memory statistics. For runtime contributors, the same behavior is split across implementation code, a pacing controller, and exported accounting structures that turn internal events into observable numbers.

Sources: src/runtime/mgc.go, src/runtime/mgcpacer.go, src/runtime/mstats.go

Go’s collector is designed around concurrent work, meaning the program and collector usually execute at the same time. That design makes the pacing controller central: it decides when a cycle should begin and how much marking work should be performed by background workers or by allocating goroutines that assist. The evidence in the pacer file names the goal as a CPU utilization fraction of GOMAXPROCS and documents a dedicated background utilization of twenty five percent. That is an implementation goal, not a user promise, but it explains why GC behavior is discussed in terms of both memory growth and processor time.

Sources: src/runtime/mgcpacer.go

Relevant Source Files

  • src/runtime/mgc.go - Main garbage collector implementation file for the runtime package; use it as the entry point when following the collection cycle and the relationship between marking, termination, sweeping, and runtime coordination.
  • src/runtime/mgcpacer.go - Implements the garbage collection pacing controller, including trigger decisions, background marking utilization, assist accounting, heap minimums, memory-limit headroom, and scan-work credit behavior.
  • src/runtime/mstats.go - Defines the runtime’s internal memory statistics, the public MemStats structure, heap and span terminology, GC pause accounting, forced GC counters, and the CPU fraction reported for garbage collection overhead.

System-to-Code Mapping

The easiest way to read the GC source is to separate policy from accounting and lifecycle mechanics. The lifecycle mechanics live in the main GC implementation file, where the runtime coordinates phases of a collection cycle with the scheduler, heap allocator, write barriers, and sweep state. The pacing policy is concentrated in the pacer file. The accounting boundary is in the memory statistics file, where internal counters are organized into the public shape read by programs. This separation matters because a symptom such as heap growth may originate in pacing, while a metric such as HeapAlloc is defined by how sweeping updates allocation state.

Sources: src/runtime/mgc.go, src/runtime/mgcpacer.go, src/runtime/mstats.go

The pacer comments describe gcController as the component that determines when to trigger concurrent garbage collection and how much marking work to do in mutator assists and background marking. In runtime terminology, a mutator is ordinary application execution that allocates, stores pointers, and changes the object graph while the collector is trying to find live objects. An assist is GC work charged to allocation, so the allocating goroutine helps the collector keep up. Background marking is scheduled collector work that aims at a fixed utilization. Together, those mechanisms allow the collector to meet a heap goal without relying only on stop-the-world work.

Sources: src/runtime/mgcpacer.go

The memory statistics file provides the vocabulary for interpreting what the runtime reports. It says MemStats records allocator statistics and explains that Alloc is the same as HeapAlloc, while TotalAlloc is cumulative and does not decrease when objects are freed. It also defines Sys as memory obtained from the operating system and explains that not all reserved address space is necessarily backed by physical memory at every moment. When diagnosing a program, this distinction prevents a common mistake: a rising cumulative allocation count is not the same as a rising live heap, and reserved address space is not the same as current object liveness.

Sources: src/runtime/mstats.go

Execution Flow

A collection cycle starts when the runtime decides the heap and allocation rate require concurrent marking. The pacer’s job is to pick that trigger early enough that mark work can complete near the heap goal. Its comments state that the controller calculates a ratio between allocation rate, expressed in CPU time, and GC scan throughput. That ratio is used to determine the heap size at which to trigger a cycle so that no assists are required to finish on time in the ideal case. In practice, assists still exist as a correction mechanism when allocation outruns background marking or when the program’s behavior differs from the controller’s model.

Sources: src/runtime/mgcpacer.go

During marking, the runtime identifies reachable objects while the application continues to allocate and update pointers. The pacer constants show the balancing act. gcCreditSlack allows scan-work credit to accumulate locally before updating global controller fields, trading exactness for reduced contention. gcAssistTimeSlack similarly batches assist time accounting on a processor before publishing it. gcOverAssistWork makes an assist perform extra scan work so the cost is amortized over future allocations. These are small internal details, but they illustrate a recurring runtime design constraint: accounting must be accurate enough for control decisions without becoming a synchronization bottleneck itself.

Sources: src/runtime/mgcpacer.go

Sweeping is the phase that turns unreachable objects back into allocatable space, and it is the phase explicitly reflected in the MemStats documentation for HeapAlloc. The memory statistics comments state that HeapAlloc increases as heap objects are allocated and decreases as the heap is swept and unreachable objects are freed. That sentence is important for interpreting observations after a collection. A completed mark phase has identified garbage, but the public allocation number decreases as sweeping processes spans. Therefore, short-term measurements can show retained allocation until sweeping catches up, even when the next allocation path can reuse freed space.

Sources: src/runtime/mstats.go

Pacing and Memory Limits

The pacer contains constants that encode the runtime’s default expectations and its memory-limit regime. defaultHeapMinimum defines the heap minimum used for a default garbage collection percentage, with an experiment-controlled selection between a smaller and larger minimum. The memory-limit constants define extra headroom for heap goals when operating under a memory limit: a minimum byte headroom and a percentage headroom. These names show that the controller is not merely reacting to a single target size. It has to maintain enough space to run efficiently while respecting externally visible memory constraints and avoiding pathological collection frequency on small heaps.

Sources: src/runtime/mgcpacer.go

The utilization constants explain why increasing GC effort is not free. The comments say increasing goal utilization shortens cycles because more resources are behind the collector and can lessen write barrier costs, but it increases mutator latency. Background utilization is fixed at twenty five percent of GOMAXPROCS, and the difference between the goal and background utilization is made up by assists. This maps directly to user experience: if the application allocates heavily, the runtime may ask allocating goroutines to perform marking work, which can appear as allocation latency rather than as one large pause.

Sources: src/runtime/mgcpacer.go

Runtime Metrics and Interpretation

The internal mstats structure records collector state such as last collection time, total pause time, circular buffers of recent pause lengths and end times, number of collections, number of forced collections, and the fraction of CPU time used by GC. The public MemStats structure then exposes allocator and heap information with detailed comments. For diagnostics, begin by deciding what question you are asking. If the question is live memory, use allocated heap fields. If the question is allocation churn, compare cumulative allocation and object counts. If the question is GC cost, look at pauses, collection counts, and CPU fraction together rather than in isolation.

Sources: src/runtime/mstats.go

The span model in MemStats also matters for source-level reasoning. The comments describe heap virtual address space as spans, contiguous regions of memory of at least eight kilobytes. A span can be idle, in use, or used for goroutine stacks, and a stack span is not considered part of the heap. This explains why allocator and operating system memory statistics do not always move together. Idle spans may retain virtual address space while physical memory can be released back to the operating system. A runtime change that improves object reclamation may therefore show up differently in heap allocation, heap idle, heap released, and system memory totals.

Sources: src/runtime/mstats.go

Practical Reading Path

When investigating GC behavior in the Go repository, start with the symptom and follow the file boundary that owns it. For a question about when collections start or why assists are occurring, read the pacer controller and its constants first. For a question about what a number in runtime memory statistics means, start in the memory statistics definitions and comments. For a question about phase ordering, collector state transitions, or how marking and sweeping coordinate with the rest of the runtime, use the main GC implementation file as the root of the trace. Keeping those entry points separate makes the runtime source more approachable.

Sources: src/runtime/mgc.go, src/runtime/mgcpacer.go, src/runtime/mstats.go

A useful next step is to connect these files to the diagnostic tools that surface their behavior. Memory profiles, execution traces, runtime metrics, and benchmark measurements can all point at garbage collection, but they answer different questions. Treat MemStats as a low-level snapshot of allocator and collector counters, and treat pacing code as the explanation for why the runtime chose a particular amount of concurrent work. If you are modifying the runtime, verify both the control behavior and the exported accounting: a pacing improvement that produces misleading statistics is still a user-visible regression for anyone diagnosing production programs.