Explore the evolution, mechanics, and optimization of Go's garbage collector, along with practical examples and best practices for memory management
Garbage collection in Go is an automatic memory management feature that helps in the efficient allocation and deallocation of memory resources during program execution. It plays a crucial role in enhancing the performance and safety of Go applications by eliminating the need for manual memory management, which can be error-prone and complex. The garbage collector in Go works by tracking and freeing memory that is no longer in use by the program, thus preventing memory leaks and optimizing memory usage. This mechanism allows developers to focus more on application logic rather than managing memory, making Go a robust language for building high-performance and concurrent applications.

1. Historical Context and Evolution of Go's Garbage Collector
The evolution of Go's garbage collector is a fascinating journey that reflects the language's commitment to performance and efficiency.
- Initial Version (Go 1.0):
- Go 1.1 Enhancements:
- Go 1.3 Improvements:
- Major Overhaul in Go 1.5 (August 2015):
- Continuous Refinements Post Go 1.5:
- Ongoing improvements in minimizing CPU time and application throughput impact.
- Enhanced GC pacing for better performance balancing.
2. Understanding Memory Management in Go
Memory management in Go is primarily handled through its automatic garbage collection mechanism.
2.1 Memory Allocation
- Dynamic Allocation: Go allocates memory dynamically for objects and data structures.
- New and Make Functions: The built-in functions
new
andmake
are typically used for allocation.new(T)
allocates memory for a variable of typeT
, initializes it to the zero value ofT
, and returns a pointer.make(T, args)
is used for slices, maps, and channels, which initializes the internal data structure and prepares the value for use.
type MyStruct struct {
Field1 int
Field2 string
}
// Using new
ptr := new(MyStruct) // Allocates memory for MyStruct, initializes it, and returns a pointer
// Using make
mySlice := make([]int, 10) // Allocates and initializes a slice of ints with length 10
2.2 Memory Deallocation
- Automatic Garbage Collection: Go uses a garbage collector (GC) to automatically deallocate memory that is no longer reachable or needed.
- No Explicit Freeing: Unlike languages like C, Go does not require the developer to explicitly free allocated memory. The garbage collector periodically runs and frees unused memory.
Consider a function in Go that creates a local slice:
func createSlice() {
localSlice := make([]int, 1000)
// use localSlice
}
When createSlice
is called, memory for localSlice
is allocated. After the function completes, localSlice
is no longer reachable since it was a local variable. The garbage collector will eventually identify this unused memory and automatically free it.
2.3 Garbage Collector Characteristics
- Concurrent Operation: The GC operates concurrently with the running program, minimizing pause times.
- Tri-color Marking Algorithm: It uses a tri-color marking algorithm for efficiency.
- Pacing Algorithm: To decide when to run, Go's GC uses a pacing algorithm that balances memory usage and GC overhead.

3. The Tricolor Mark-and-Sweep Algorithm in Go's Garbage Collector
The tricolor mark-and-sweep algorithm is an efficient method used by Go's garbage collector (GC) for memory management. It categorizes objects in the heap into three colors: white, grey, and black, representing their states during the garbage collection cycle.
How It Works
- Initial State:
- All heap objects start as white, indicating they are unprocessed.
- Marking Phase:
- Roots (like global variables, stack items) are identified and marked grey. These are the entry points to the heap.
- A grey object is reachable but not yet fully processed.
- The GC scans grey objects; if a grey object references other white objects, those turn grey.
- Once scanned, the object turns black, indicating it is in use and has been fully processed.
- Sweep Phase:
- After marking, all black objects are considered in use, and white objects are deemed unreachable.
- The GC then 'sweeps' the white objects, freeing the memory they occupied.
Here is a sample code how Go's garbage collector handles circular references:
package main
import (
"fmt"
"runtime"
)
type Object struct {
reference *Object
}
func main() {
// Initial setup
obj1 := &Object{} // This object will be marked as grey initially
obj2 := &Object{reference: obj1} // Another object referencing obj1
// This is where the tri-color algorithm kicks in
obj1.reference = obj2 // Creating a circular reference
// Unsetting obj1 and obj2 to make them eligible for GC
obj1 = nil
obj2 = nil
// Forcing a garbage collection cycle
runtime.GC()
fmt.Println("Garbage Collection executed")
}
Here's a breakdown of how the tricolor algorithm applies to above code:
- Initial State:
obj1
andobj2
are created. Initially, they would be considered white (unmarked) in the tricolor GC context.
- Circular Reference Setup:
- We create a circular reference between
obj1
andobj2
. This means that each object is referencing the other, which could potentially lead to a memory leak in a garbage collector that doesn't handle circular references well.
- We create a circular reference between
- Unsetting References:
- By setting
obj1
andobj2
tonil
, we've effectively removed any root references to these objects. In the tricolor algorithm, this would mean that there are no grey objects leading to these instances, making them candidates for garbage collection.
- By setting
- Garbage Collection:
- When
runtime.GC()
is called, Go's garbage collector will run. Despite the circular reference, the GC will identify that there are no active references (grey objects pointing) toobj1
andobj2
from any roots (like global or stack variables). Thus, they both become eligible for garbage collection.
- When
- GC Execution:
- The GC, using its mark-and-sweep algorithm, will mark these objects as collectible (remaining white, as they are unreachable) and will eventually sweep (clean up) this memory.
4. How Golang Garbage Collector can impact performance?
The garbage collector in Go can have an impact on the performance of a program, depending on how it is used and configured. Here are a few ways in which the garbage collector can impact performance:
- GC Pause Time: Garbage collection in Go is done concurrently with the program's execution. However, there will be brief pauses in the program's execution while the garbage collector is running. These pauses can be noticeable if the program is running a performance-sensitive workload, such as a real-time application.
- Memory Allocation: The garbage collector can cause additional memory allocation and deallocation, which can increase the program's memory usage and cause cache misses, leading to increased CPU usage.
- CPU usage: The garbage collector uses some CPU time to identify and reclaim unreachable objects, this could cause performance issues if the program is running a CPU-bound workload.
- GC trigger threshold: The garbage collector's trigger threshold is configurable via the runtime. The GOGC variable sets the initial garbage collection target percentage. If the trigger threshold is set too low, the garbage collector may run too frequently, leading to increased CPU usage and GC pause time. If the trigger threshold is set too high, the program may run out of memory before the garbage collector runs.
- Memory Pressure: If your program is under memory pressure, it can cause a performance hit. Even though the Go garbage collector is efficient at releasing memory, if the program's memory usage is too high, it can cause the garbage collector to run frequently, and the program could run out of memory.

5. Practical Examples using Garbage Collector in GO
Example-1: How memory allocation works in golang
Here is a simple golang example where the string object "hello, world
" is allocated memory when the variable s
is declared. As long as the variable s
is in scope, the string object is considered reachable and will not be garbage collected. Once the variable s
goes out of scope, the string object is no longer reachable and will be eligible for garbage collection.
package main
import "fmt"
func main() {
// Allocate memory for a new string object
s := "hello, world"
// The variable s is still in scope, so the string object it references is considered reachable
fmt.Println(s)
// Once the variable s goes out of scope, the string object it references is no longer reachable
// and will be eligible for garbage collection
}
Example-2: Manually trigger garbage collector using runtime.GC()
In this example we use runtime.GC() to manually trigger the garbage collector:
package main
import (
"fmt"
"runtime"
)
func main() {
// Allocate some memory for the program to use
s := make([]string, 0, 100000)
for i := 0; i < 100000; i++ {
s = append(s, "hello, world")
}
// Print the initial memory usage
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Println("Initial HeapAlloc: ", m.HeapAlloc)
// Trigger the garbage collector
runtime.GC()
// Print the memory usage after the garbage collector has run
runtime.ReadMemStats(&m)
fmt.Println("After GC HeapAlloc: ", m.HeapAlloc)
// Release the memory
s = nil
// Trigger the garbage collector
runtime.GC()
// Print the memory usage after the garbage collector has run
runtime.ReadMemStats(&m)
fmt.Println("After release HeapAlloc: ", m.HeapAlloc)
}
This program uses the runtime
package to manually trigger the garbage collector and then prints the heap allocation before and after the garbage collection.
Output:
# go run main.go
Initial HeapAlloc: 1654512
After GC HeapAlloc: 37872
After release HeapAlloc: 37872
Example-3: Display Heap Stats before and after garbage collector is executed
Here is another example of how we can use the runtime.ReadMemStats()
function to retrieve statistics about the program's memory usage and monitor the garbage collector:
package main
import (
"fmt"
"runtime"
)
func main() {
var m runtime.MemStats
// Print statistics before the slice is released
runtime.ReadMemStats(&m)
fmt.Println("HeapAlloc: ", m.HeapAlloc)
fmt.Println("HeapIdle: ", m.HeapIdle)
fmt.Println("HeapReleased: ", m.HeapReleased)
fmt.Println("NumGC: ", m.NumGC)
fmt.Println("-----------")
// Allocate some memory for the program to use
s := make([]string, 0, 100000)
for i := 0; i < 100000; i++ {
s = append(s, "hello, world")
}
// Print statistics after the slice is released
runtime.ReadMemStats(&m)
fmt.Println("HeapAlloc: ", m.HeapAlloc)
fmt.Println("HeapIdle: ", m.HeapIdle)
fmt.Println("HeapReleased: ", m.HeapReleased)
fmt.Println("NumGC: ", m.NumGC)
fmt.Println("-----------")
// Release the memory
s = nil
// Manually trigger garbage collector
runtime.GC()
runtime.ReadMemStats(&m)
fmt.Println("HeapAlloc: ", m.HeapAlloc)
fmt.Println("HeapIdle: ", m.HeapIdle)
fmt.Println("HeapReleased: ", m.HeapReleased)
fmt.Println("NumGC: ", m.NumGC)
fmt.Println("-----------")
}
In this example we create a large slice of string which can reserve some memory and then retrieve the memory statistics using the runtime.ReadMemStats()
function. The statistics include the amount of memory that is currently allocated (HeapAlloc
), the amount of memory that is currently unused and available (HeapIdle
), the amount of memory that has been released to the operating system (HeapReleased
) and the number of garbage collection runs (NumGC
).
After this, we release the slice by setting it to nil
and allow the garbage collector to reclaim the memory. Then we call runtime.GC()
to manually trigger the garbage collector and retrieve the statistics again.
As we can see, after releasing the slice, the HeapAlloc
and NumGC
decreases, and the HeapReleased
and HeapIdle
increases.
# go run main.go
HeapAlloc: 48048
HeapIdle: 3719168
HeapReleased: 3686400
NumGC: 0
-----------
HeapAlloc: 1654104
HeapIdle: 2072576
HeapReleased: 2072576
NumGC: 0
-----------
HeapAlloc: 37648
HeapIdle: 3710976
HeapReleased: 2039808
NumGC: 1
-----------
Also NOTE that the garbage collector runs are not guaranteed to happen at a specific time, so we may need to wait for a while or manually trigger the garbage collector using runtime.GC()
to see the expected results.
We can also use the statistics to detect potential memory leaks and performance issues, also we can use HeapAlloc
and HeapReleased
to check if our program is releasing memory correctly.
Example-4: Using GODEBUG to debug golang garbage collector
GODEBUG is the controller of the variables and is used for debugging within the Go runtime. This variable contains a list of name=val
key-value pairs, separated by commas. These named variables are used to tune the output of the debugging information the binary will return. Here is another piece of code which tries to free unused memory efficiently:
package main
import (
"fmt"
"runtime"
"time"
)
func printStats(mem runtime.MemStats) {
runtime.ReadMemStats(&mem)
fmt.Println("mem.Alloc:", mem.Alloc)
fmt.Println("mem.TotalAlloc:", mem.TotalAlloc)
fmt.Println("mem.HeapAlloc:", mem.HeapAlloc)
fmt.Println("mem.NumGC:", mem.NumGC)
fmt.Println("-----")
}
func main() {
var mem runtime.MemStats
printStats(mem)
for i := 0; i < 10; i++ {
s := make([]byte, 100000000)
if s == nil {
fmt.Println("Operation failed!")
}
}
printStats(mem)
for i := 0; i < 10; i++ {
s := make([]byte, 100000000)
if s == nil {
fmt.Println("Operation failed!")
}
time.Sleep(5 * time.Second)
}
printStats(mem)
}
We use for loop to obtain large amounts of memory in order to trigger the use of garbage collector.
Output:
# go run main.go
mem.Alloc: 48256
mem.TotalAlloc: 48256
mem.HeapAlloc: 48256
mem.NumGC: 0
-----
mem.Alloc: 100045200
mem.TotalAlloc: 1000128496
mem.HeapAlloc: 100045200
mem.NumGC: 9
-----
^Csignal: interrupt
So, the output presents information about properties related to the memory used by the main.go
program. If we want to get an even more detailed output, we can execute the same tool using GODEBUG
, as shown here:
# GODEBUG=gctrace=1 go run main.go a
gc 1 @0.019s 1%: 0.014+2.4+0.001 ms clock, 0.014+0.33/0/0+0.001 ms cpu, 4->4->0 MB, 5 MB goal, 1 P
gc 2 @0.050s 3%: 0.027+5.1+0.002 ms clock, 0.027+0.37/1.0/0+0.002 ms cpu, 4->4->1 MB, 5 MB goal, 1 P
gc 3 @0.089s 2%: 0.067+3.3+0.002 ms clock, 0.067+0.66/0/0+0.002 ms cpu, 4->4->1 MB, 5 MB goal, 1 P
gc 4 @0.128s 2%: 0.032+2.6+0.003 ms clock, 0.032+0.82/0/0+0.003 ms cpu, 4->4->1 MB, 5 MB goal, 1 P
gc 5 @0.153s 2%: 0.046+5.1+0.002 ms clock, 0.046+0.81/0/0+0.002 ms cpu, 4->5->1 MB, 5 MB goal, 1 P
gc 6 @0.175s 3%: 0.030+11+0.002 ms clock, 0.030+1.4/0.16/0+0.002 ms cpu, 4->5->1 MB, 5 MB goal, 1 P
gc 7 @0.224s 2%: 0.027+2.4+0.003 ms clock, 0.027+0.63/0/0+0.003 ms cpu, 4->5->2 MB, 5 MB goal, 1 P
# command-line-arguments
gc 1 @0.004s 17%: 0.009+2.4+0.002 ms clock, 0.009+1.3/0/0+0.002 ms cpu, 4->6->5 MB, 5 MB goal, 1 P
gc 2 @0.036s 16%: 0.014+8.7+0.004 ms clock, 0.014+4.0/2.2/0+0.004 ms cpu, 9->9->8 MB, 11 MB goal, 1 P
mem.Alloc: 48128
mem.TotalAlloc: 48128
mem.HeapAlloc: 48128
mem.NumGC: 0
-----
gc 1 @0.007s 1%: 0.011+0.11+0.002 ms clock, 0.011+0.10/0/0+0.002 ms cpu, 95->95->0 MB, 96 MB goal, 1 P
gc 2 @0.054s 0%: 0.030+0.13+0.002 ms clock, 0.030+0.12/0/0+0.002 ms cpu, 95->95->0 MB, 96 MB goal, 1 P
gc 3 @0.106s 0%: 0.023+0.12+0.002 ms clock, 0.023+0.12/0/0+0.002 ms cpu, 95->95->0 MB, 96 MB goal, 1 P
gc 4 @0.141s 0%: 0.023+0.15+0.004 ms clock, 0.023+0.15/0/0+0.004 ms cpu, 95->95->0 MB, 96 MB goal, 1 P
gc 5 @0.185s 0%: 0.021+0.12+0.001 ms clock, 0.021+0.11/0/0+0.001 ms cpu, 95->95->0 MB, 96 MB goal, 1 P
gc 6 @0.221s 0%: 0.023+0.22+0.002 ms clock, 0.023+0.22/0/0+0.002 ms cpu, 95->95->0 MB, 96 MB goal, 1 P
gc 7 @0.269s 0%: 0.025+0.12+0.001 ms clock, 0.025+0.12/0/0+0.001 ms cpu, 95->95->0 MB, 96 MB goal, 1 P
gc 8 @0.311s 0%: 0.032+0.33+0.002 ms clock, 0.032+0.32/0/0+0.002 ms cpu, 95->95->0 MB, 96 MB goal, 1 P
gc 9 @0.350s 0%: 0.022+0.10+0.006 ms clock, 0.022+0.097/0/0+0.006 ms cpu, 95->95->0 MB, 96 MB goal, 1 P
gc 10 @0.390s 0%: 0.021+0.11+0.005 ms clock, 0.021+0.10/0/0+0.005 ms cpu, 95->95->0 MB, 96 MB goal, 1 P
mem.Alloc: 100045256
mem.TotalAlloc: 1000128368
mem.HeapAlloc: 100045256
mem.NumGC: 9
-----
Let's understand this output:
Output | Description |
gc 1 | The garbage collection number. This number is incremented at each garbage collection. |
@0.007s | This garbage collection occurred 0.007s s after the program was started. |
0% | The percentage of time spent in GC since the program was started. |
0.011+0.11+0.002 ms clock | The wallclock/CPU times that occur for the phases of the GC. This can be expressed as Tgc = Tseq + Tmark + Tsweep. Tseq: User Go routines time stop (stop the world sweep termination). Tmark: The heap making time (concurrent mark and scan time). Tsweep: Heap sweeping time (sweep the world mark termination). |
95->95->0 MB | GC start, GC end, and live heap sizes. This shows how well or how badly the garbage collector is doing |
96 MB goal | The goal heap size. |
1 P | 4 processors being used. |
Example-5: Using pprof
for Garbage Collection Analysis
pprof
Overview: pprof
is a profiling tool in Go that helps in analyzing the performance of a Go program. It can provide insights into memory usage, CPU profiling, and understanding garbage collection behavior.
package main
import (
"log"
"net/http"
_ "net/http/pprof"
"time"
)
func main() {
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// Simulate some work
for {
work()
time.Sleep(1 * time.Second)
}
}
func work() {
_ = make([]byte, 10*1024*1024) // Allocating memory
}
In this example:
- We import
net/http/pprof
, which automatically registers pprof handlers to the default HTTP server. - The program starts an HTTP server at
localhost:6060
where the pprof tool can be accessed. - The
work
function simulates memory allocation. - To view memory profiling, you can navigate to
http://localhost:6060/debug/pprof/heap
.
The runtime
package in Go provides functions to retrieve garbage collection metrics. Here's an example:
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
var stats runtime.MemStats
for {
runtime.ReadMemStats(&stats)
fmt.Printf("Alloc = %v MiB", stats.Alloc / 1024 / 1024)
fmt.Printf("\tTotalAlloc = %v MiB", stats.TotalAlloc / 1024 / 1024)
fmt.Printf("\tSys = %v MiB", stats.Sys / 1024 / 1024)
fmt.Printf("\tNumGC = %v\n", stats.NumGC)
time.Sleep(5 * time.Second)
}
}
In this code:
- We use
runtime.ReadMemStats
to read memory statistics. Alloc
,TotalAlloc
,Sys
, andNumGC
give us information about current memory allocation, total memory allocation, system memory, and the number of garbage collections that have occurred, respectively.
6. Summary
Go supports Garbage Collection (GC) so we do not have to deal with memory allocation and deallocation. However, GC might slow down our programs a little. We learned how memory is allocated inside a go program and how we can read statistics of different type of memory used, released with runtime
package. We learned how we can use the runtime.ReadMemStats()
function to monitor the program's memory usage and the garbage collector's performance.
Here are links which can help you learn and understand more about Golang garbage collector:
A Guide to the Go Garbage Collector
Getting to Go: The Journey of Go's Garbage Collector