Table of Contents
Introduction to Garbage Collection in Golang
As per wikipedia source garbage collection (GC) is a form of automatic memory management. The garbage collector attempts to reclaim memory which was allocated by the program, but is no longer referenced; such memory is called garbage.
Go's garbage collection is a mechanism for automatically releasing memory that is no longer needed by a program. The Go runtime uses a garbage collector to periodically scan the program's heap (the area of memory where objects are allocated), looking for objects that are no longer reachable. Once the garbage collector determines that an object is no longer reachable, it will release the memory associated with that object and make it available for reuse.
Go's garbage collector uses a concurrent, stop-the-world algorithm. This means that while the garbage collector is running, the program is paused and cannot continue execution. The duration of the pause is typically very short and should not be noticeable to the user.
The Go garbage collector uses a technique called "tracing" to determine which objects are still reachable. The garbage collector starts with a set of "root" objects, such as global variables, and then "traces" the pointers from these objects to other objects, marking all of the objects that are reachable. Any objects that are not marked as reachable are considered garbage and are eligible for collection.
Go also uses a technique called "generational" garbage collection, which means that the garbage collector divides the heap into different generations, and it focuses on collecting objects from the older generations first. This strategy will yield better performance in most cases, as most objects are short-lived and will be collected quickly.
Enough with description, let's take some practical examples to understand more about golang garbage collector
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.
How golang Garbage Collector works?
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. |
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.
Further Reading
A Guide to the Go Garbage Collector
Getting to Go: The Journey of Go's Garbage Collector