9 min read

Did you ever think of how bulk messages are pushed in real-time that fast? How is it possible? Low latency garbage collector plays an important role in this. In this article, we present ways to look at certain parameters to implement memory management with the Golang garbage collector.

Garbage collection is the process of freeing up memory space that is not being used. In other words, the garbage collector sees which objects are out of scope and cannot be referenced anymore and frees the memory space they consume. This process happens in a concurrent way while a Go program is running and not before or after the execution of the program.

This article is an excerpt from the book Mastering Go – Second Edition by Mihalis Tsoukalos. Mihalis runs through the nuances of Go, with deep guides to types and structures, packages, concurrency, network programming, compiler design, optimization, and more. 

The main concern of the Go garbage collector is low latency, which basically means short pauses in its operation in order to have a real-time operation. On the other hand, what a program does is create new objects and manipulate existing objects with pointers all the time. This process can end up creating objects that cannot be accessed anymore because there are no pointers pointing to these objects. These objects are then garbage and wait for the garbage collector to clean them up and free their memory space. After that, the memory space that has been freed is ready to be used again.


Implementing the Golang garbage collector

The Go standard library offers functions that allow you to study the operation of the garbage collector and learn more about what the garbage collector does secretly. The relevant code is saved as gColl.go and will be presented in three parts.

The first code segment of gColl.go is the following:

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("-----")

}

Note that each time you need to get the more recent garbage collection statistics, you will need to call the runtime.ReadMemStats() function. The purpose of the printStats() function is to avoid writing the same Go code all the time.

The second part of the program is the following:

func main() {

   var mem runtime.MemStats

   printStats(mem)




   for i := 0; i 

The for loop creates many big Go slices in order to allocate large amounts of memory and trigger the garbage collector.

The last part of gColl.go comes with the next Go code, which does more memory allocations using Go slices:

for i := 0; i 

Here’s how the output of gColl.go on a macOS Mojave machine would look like:

$ go run gColl.go

mem.Alloc: 66024

mem.TotalAlloc: 66024

mem.HeapAlloc: 66024

mem.NumGC: 0

-----

mem.Alloc: 50078496

mem.TotalAlloc: 500117056

mem.HeapAlloc: 50078496

mem.NumGC: 10

-----

mem.Alloc: 76712

mem.TotalAlloc: 1500199904

mem.HeapAlloc: 76712

mem.NumGC: 20

-----

Although you are not going to examine the operation of the Go garbage collector all the time, being able to watch the way that it operates on a slow application can save so much time in the long run. I can assure you that you will not regret the time you spend learning about garbage collection in general and, more specifically, about the way the Go garbage collector works.

There is a trick that allows you to get an even more detailed output about the way the Go garbage collector operates, which is illustrated in the next command:

$ GODEBUG=gctrace=1 go run gColl.go

So, if you put GODEBUG=gctrace=1 in front of any go run command, Go will print analytical data about the operation of the garbage collector. The data will be in this form:

gc 4 @0.025s 0%: 0.002+0.065+0.018 ms clock,
0.021+0.040/0.057/0.003+0.14 ms cpu, 47->47->0 MB, 48 MB goal, 8 P

gc 17 @30.103s 0%: 0.004+0.080+0.019 ms clock,
0.033+0/0.076/0.071+0.15 ms cpu, 95->95->0 MB, 96 MB goal, 8 P

Depending on the system requirement the trinity of values may differ. For example, 47->47->0 MB trinity of values from the preceding output gives us more information about the heap sizes during the garbage collection process. The first number is the heap size when the garbage collector is about to run. The second value is the heap size when the garbage collector ends its operation. The last value is the size of the live heap.

Go garbage collection is based on the tricolor algorithm

The operation of the Go garbage collection is based on the tricolor algorithm, which is the subject of this subsection.

Strictly speaking, the official name for the algorithm used in Go is the tricolor mark-and-sweep algorithm. It can work concurrently with the program and uses a write barrier. This means that when a Go program runs, the Go scheduler is responsible for the scheduling of the application and the garbage collector. This is as if the Go scheduler has to deal with a regular application with multiple goroutines!

The core idea behind this algorithm came from Edsger W. Dijkstra, Leslie Lamport, A. J. Martin, C. S. Scholten, and E. F. M. Steffens and was first illustrated in a paper named On-the-Fly Garbage Collection: An Exercise in Cooperation.

The primary principle behind the tricolor mark-and-sweep algorithm is that it divides the objects of the heap into three different sets according to their color, which is assigned by the algorithm. It is now time to talk about the meaning of each color set. The objects of the black set are guaranteed to have no pointers to any object of the white set. However, an object of the white set can have a pointer to an object of the black set because this has no effect on the operation of the garbage collector. The objects of the gray set might have pointers to some objects of the white set. Finally, the objects of the white set are the candidates for garbage collection.

Please note that no object can go directly from the black set to the white set, which allows the algorithm to operate and be able to clear the objects on the white set. Additionally, no object of the black set can directly point to an object of the white set.

So, when the garbage collection begins, all objects are white, and the garbage collector visits all the root objects and colors them gray. The roots are the objects that can be directly accessed by the application, which includes global variables and other things on the stack. These objects mostly depend on the Go code of a program.

After that, the garbage collector picks a gray object, makes it black, and starts looking at whether that object has pointers to other objects of the white set. This means that when a gray object is being scanned for pointers to other objects, it is colored black. If that scan discovers that this object has one or more pointers to a white object, it puts that white object in the gray set. This process keeps going for as long as there exist objects in the gray set. After that, the objects in the white set are unreachable and their memory space can be reused. Therefore, at this point, the elements of the white set are said to be garbage collected.

During this process, the running application is called the mutator. The mutator runs a small function named write barrier that is executed each time a pointer in the heap is modified. If the pointer of an object in the heap is modified, which means that this object is now reachable, the write barrier colors it gray and puts it in the gray set.

As a result, the heap is pictured as a graph of connected objects, which is also shown in Figure 1 that demonstrates a single phase of a garbage collection cycle.

Figure 1: The Go garbage collector represents the heap of a program as a graph

So, there exist three different colors: black, white, and gray. When the algorithm begins, all objects are colored white. As the algorithm keeps going, white objects are moved into one of the other two sets. The objects that are left in the white set are the ones that are going to be cleared at some point.

In the presented graph, you can see that while object E, which is in the white set, can access object F, it cannot be accessed by any other object because no other object points to object E, which makes it a perfect candidate for garbage collection! Additionally, objects A, B, and C are root objects and are always reachable; therefore, they cannot be garbage collected.

Graph comprehended

Can you guess what will happen next in that graph? Well, it is not that difficult to realize that the algorithm will have to process the remaining elements of the gray set, which means that both objects A and F will go to the black set. Object A will go to the black set because it is a root element and F will go to the black set because it does not point to any other object while it is in the gray set.

After object A is garbage collected, object F will become unreachable and will be garbage collected in the next cycle of the garbage collector because an unreachable object cannot magically become reachable in the next iteration of the garbage collection cycle.

The Go garbage collection can also be applied to variables such as channels. When the garbage collector finds out that a channel is unreachable, that is when the channel variable cannot be accessed anymore, it will free its resources even if the channel has not been closed.

Go allows you to manually initiate a garbage collection by putting a runtime.GC() statement in your Go code. However, have in mind that runtime.GC() will block the caller and it might block the entire program, especially if you are running a very busy Go program with many objects. This mainly happens because you cannot perform garbage collections while everything else is rapidly changing, as this will not give the garbage collector the opportunity to clearly identify the members of the white, black, and gray sets. This garbage collection status is also called garbage collection safe-point.

Garbage Collectors in Go are either optimized for lower latency or higher throughput. They might also perform better or worse at these depending on the heap usage of your program. Tricolor mark-and-sweep algorithm is used in Go to lower that latency by running the garbage collector as a concurrent process.

Dive deep into the Go language and become an expert Go developer from our latest book Mastering Go – Second Edition written by Mihalis Tsoukalos.

Read Next

Ian Lance Taylor, Golang team member, adds another perspective to Go being Google’s language

Implementing hashing algorithms in Golang [Tutorial]

Is Golang truly community driven and does it really matter?