How Golang Garbage Collector Works? [Tutorial]


Written By - admin
Advertisement

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.

Advertisement
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

 

Can't find what you're searching for? Let us assist you.

Enter your query below, and we'll provide instant results tailored to your needs.

If my articles on GoLinuxCloud has helped you, kindly consider buying me a coffee as a token of appreciation.

Buy GoLinuxCloud a Coffee

For any other feedbacks or questions you can either use the comments section or contact me form.

Thank You for your support!!

Leave a Comment