The original link

When programs run, they write objects to memory. At some point, when these objects are no longer needed, they should be removed. This process is called memory management. This article aims to provide an overview of memory management, followed by an in-depth look at how it is implemented through garbage collection in Go. You’ve seen a lot of memory management changes in Go over the past few years, and you’ll probably see more in the future. If you’re reading this and you’re using Go V1.16 or later, some of the information in this article may be out of date.

Manual Memory Management

In languages like C, developers call functions like malloc or calloc to write objects to memory. These functions return a pointer to the object’s location in heap memory. When the object is no longer needed, the developer calls the free function to reuse the memory. This approach to memory management, called explicit unallocation, is very powerful. It gives the developer more control over the memory being used, which allows some types of simple optimizations, especially in low-memory environments. However, it leads to two types of programming errors.

One is calling the free function prematurely, creating a dangling pointer. Suspended Pointers are Pointers that no longer point to legitimate objects in memory. This is bad because the program expects the pointer to point to a defined value. When this pointer is subsequently accessed, there is no guarantee that the value it points to exists in the corresponding memory address, which may be nothing, or some other value entirely. Second, memory is not released. If a developer forgets to release an object, it can cause a memory leak as memory fills up with more and more objects. If you overuse memory, your program can slow down or crash. When memory is explicitly managed, unpredictable bugs can be introduced into the program.

Automatic memory management

This is why languages like Go provide automated dynamic memory management, or simply garbage collection. Using a language with garbage collection has the following advantages:

  • Security enhancement

  • Better cross-platform portability

  • Write less code

  • Runtime detection of code

  • Data boundary checking

Garbage collection has a performance cost, but not as much as one might think. The advantage of this is that developers can focus on their application’s business logic and make sure it meets the requirements, without having to worry about memory management.

A running program stores objects in two memory locations, the heap and the stack. Garbage collection operates on the heap, not the stack. A stack is a LIFO data structure that stores function values. Calling another function within one function pushes a new function frame onto the stack, which contains the value of the function and other information. When the called function returns, its stack frame pops out of the stack. You may be familiar with stacks from a disintegrating program you are debugging. To aid debugging, compilers in most languages return stack trace information that shows all functions called up to this point.

In contrast to the stack, the heap contains values that are referenced outside the function. For example, constants defined when a program is started, or more complex objects such as structs in Go. When a developer defines an object in the heap, it allocates the required memory and returns a pointer to that memory. A heap is a graph in which objects are represented as nodes that are referenced in code or by other objects in the heap. When a program starts, the heap gets bigger and bigger as the objects grow, unless the heap is cleaned up.

GoGarbage collection in

Go tends to allocate memory on the stack, so most allocation ends up there. This means that each goroutine in Go has a stack, and Go assigns variables to that stack when possible. The Go compiler attempts to use escape analysis to determine whether the object escaped the function, proving that the variable is not needed outside the function. If the compiler can determine the lifetime of a variable, it is assigned to a stack. If the variable has an unpredictable lifetime, it will be allocated to the heap. In general, if a Go program has a pointer to an object, that object will be stored on the heap. Take a look at this code example:

type myStruct struct {
   value int
}

var testStruct = myStruct{value: 0}

func addTwoNumbers(a int, b int) int {
   return a + b
}
func myFunction(a) {
   testVar1 := 123
   testVar2 := 456
   testStruct.value = addTwoNumbers(testVar1, testVar2)
}
func someOtherFunction(a) {
   // some other code
   myFunction()
   // some more code
}
Copy the code

To understand the purpose of this example, let’s imagine that this code is part of a running program, because if it were the whole program, Go’s compiler would optimize to assign variables to the stack. When the program starts:

TestStruct is defined and allocated to an available block of memory on the heap

2. The myFunction function is executed and a stack is allocated when executed. TestVar1 and testVar2 are stored on the stack

  1. When addTwoNumbers is called, a new frame and two parameters are pushed onto the stack

  2. When addTwoNumbers finishes executing, its result is returned to the myFunction function and its stack frame pops off the stack because it is no longer needed

  3. Follow the pointer to testStruct to its position on the heap containing it, updating the value field.

6. MyFunction exits and cleans up the stack created for it. The value of testStruct remains on the heap until it is garbage collected

TestStruct is currently on the heap, and without any analysis, the Go runtime does not know if it is still needed. To do this, Go relies on the garbage collector. The garbage collector has two important parts, the Mutator and the collector. The collector performs collection logic while looking for objects that need to be freed. The applicator executes the application code, allocates new objects on the heap, and updates remaining objects during the program’s run, including making objects that are no longer needed inaccessible.

GoImplementation of garbage collector

Go garbage collection features non-generational concurrency, tri-color tag sweep, let’s get those words straight.

Generational theory assumes that objects with short lifetimes, such as temporary variables, are most often recycled. Therefore, a generational garbage collector focuses on the most recently allocated objects. However, as we mentioned earlier, the Go compiler’s optimizations allow the compiler to assign objects with known lifecycles to the stack. This means that the fewer objects allocated on the heap, the fewer objects are garbage collected. This means that generational recycling is not necessary in Go. Therefore, Go adopts a non-generational recycling approach. Parallelism means that the collector runs simultaneously as multiple assignment threads. Therefore, Go adopts the method of non-generational parallel recycling. Mark sweep is one way of garbage collection, and three-color mark sweep is an implementation of this way.

A mark sweep collector has two phases, not surprisingly named Mark and sweep. In the marking phase, the collector traverses the heap and marks objects that are no longer needed. The subsequent sweep phase removes these objects. Tag sweeping is an indirect algorithm because it marks only the surviving objects and removes all others.

GoStep by step:

Go uses a process called Stop the World to bring all Goroutines to a safe point in time for garbage collection. It suspends the program and opens a write barrier to keep the heap secure. It achieves concurrency by allowing goroutine and collector to run simultaneously.

Once all goroutines open the write barrier, the Go runtime starts the world and lets the worker perform garbage collection.

Based on tricolor algorithm to realize the marking. When marking begins, all objects are marked white except Roots(the root object), which is marked gray. Roots are the source of all other objects in the heap and are instantiated as part of the permit program. The garbage collector scans the stack, global variables, and heap pointer to start markers to see how much memory is being used. When a stack is scanned, the worker threads the goroutine of the stack temporarily, and marks all objects found by traversing down from Roots in gray. Goroutine was then restored.

The gray objects are then queued to become black, indicating that they are still in use. Once all gray objects have been turned black, the collector stops the world again and starts cleaning up all white nodes that are no longer needed. The program can now continue running until it needs to clean up more memory again.

The procedure is initialized again once an extra percentage of the memory the program is using has been allocated. The source code for the GOGC environment variable that defines this ratio and defaults to 100.Go describes it as follows:

If GOGC=100 and we have already used 4M memory, we will start garbage collection again when the memory in use reaches 8M (this flag is tracked in the nexT_GC variable). This makes GC costs linearly proportional to allocation costs. Tweaking the GOGC only changes the linear constant (and the amount of extra memory used).

Go garbage collection improves your programming efficiency by taking abstract memory to runtime. It’s also part of what makes Go perform so well. Go has built-in tools that allow you to optimize how garbage collection is triggered in your application. If you’re interested, you can investigate further. Now, I hope you’ve learned more about how garbage collection works and is implemented in Go.

reference

Garbage Collection in Go: Part 1

Getting to Go: The Journey of Go’s Garbage Collector

Go: How Does the Garbage Collector Mark the Memory?

Golang: Cost of using the heap

Golang FAQ

Google Groups discussion, comment by Ian Lance Taylor

Implementing memory management with Golang’s garbage collector

Memory Management Reference

Stack (abstract data type)

The Garbage Collection Handbook

Tracing garbage collection: Tri-color marking