Original text: medium.com/a-journey-w…

This article is based on Go 1.13

Go’s garbage collector is designed to help developers automatically clean up their applications’ memory. However, every time memory is traced and cleaned, the performance of the program is affected. Go’s garbage collector is designed to clean up memory as well as focus on performance, focusing on the following metrics:

  • Minimize the number of two phases when the program is paused.
  • A garbage collection cycle is less than 10ms
  • A single garbage collection operation cannot consume more than 25% of the CPU

This seems like a very difficult goal to achieve, and this article explains how Go does it.

The Heap Threshold Reached

The first metric the garbage collector looks at is heap growth. By default, the garbage collector is started when the heap size doubles. Here’s an example of a loop that keeps allocating memory

func BenchmarkAllocationEveryMs(b *testing.B) {
	// need permanent allocation to clear see when the heap double its size
	var s *[]int
	tmp := make([]int.1100000.1100000)
	s = &tmp

	var a *[]int
	for i := 0; i < b.N; i++  {
		tmp := make([]int.10000.10000)
		a = &tmp

		time.Sleep(time.Millisecond)
	}
	_ = a
	runtime.KeepAlive(s)
}
Copy the code

The trace curve tells us that the garbage collector is triggered

When the heap doubles in size, the memory allocator triggers the garbage collector. This can also be printed out by adding the parameter GODEBUG=gctrace=1

gc 8 @0.251s 0% :0.004+0.11+0.003 ms clock, 0.036+0/0.10/0.15+0.028 ms cpu, 16->16->8 MB, 17 MB goal, 8 P

gc 9 @0.389s 0% :0.005+0.11+0.007 ms clock, 0.041+0/0.090/0.11+0.062 ms cpu, 16->16->8 MB, 17 MB goal, 8 P

gc 10 @0.526s 0% :0.046+0.24+0.014 ms clock, 0.37+0/0.14/0.23+0.11 ms cpu, 16->16->8 MB, 17 MB goal, 8 P
Copy the code

Period 9 is the 389ms cycle we saw earlier. The interesting part is this: 16->16->8 MB, which shows how much memory is being used before the garbage collection and how much memory is left after the garbage collection. We clearly see that cycle 9 has been triggered at 16MB while cycle 8 has reduced the heap to 8MB.

This threshold is set by the environment variable GOGC, which defaults to 100%, meaning that the garbage collector is triggered when the heap size increases by 100%. For performance reasons, and to avoid constantly starting new garbage collections, garbage collection is not triggered when the heap size is less than 4MB*GOGC, even though GOGC is set to 100%

Time Threshold Reached

The second garbage collector focuses on the interval between garbage collection times, and if it is greater than 2 minutes, it will enforce garbage collection.

This can be seen with the given GODEBUG parameter, and the program performs a mandatory garbage collection two minutes later

GC forced
gc 15 @121.340s 0% :0.058+1.2+0.015 ms clock, 0.46+0/2.0/4.1+0.12 ms cpu, 1->1->1 MB, 4 MB goal, 8 P
Copy the code

To assist the Required Assistance

The garbage collector consists of two parts

  • Tag memory is still in use
  • Replace memory that is not marked as being in use

During the marking phase, Go must ensure that marking memory is faster than allocating new memory. In fact, if the collector marks 4Mb of memory and the program allocates the same amount of memory during the same time period, the garbage collector must be triggered immediately after completion.

To solve this problem, Go tracks new memory allocations while marking memory, and looks to see when the garbage collector needs to be triggered. When garbage collection is triggered, the first step is to prepare a Goroutine for each processor (P in GMP). This Gourtine is originally designed to deal with the dormant state, waiting for the marking phase to take place.

The trace can show these goroutines

Once these goroutinues are generated, the garbage collector will start marking them up to check which variables need to be collected and replaced. Goroutines marked GC dedicated are marked only if they are not preempted, whereas goroutines marked GC idle are marked directly because they have nothing else to run and can be preempted.

The garbage collector is now ready to mark variables as no longer in use. For each variable scan, a counter is added to keep track of how much work remains to be done. When goroutine work is scheduled during garbage collection, Go compares the required memory allocation to the scans that have already been completed in order to compare scan speed and allocation requirements. If the scan speed is faster than the allocated speed, no additional assistance is required. Conversely, if the scan speed is slower than the memory allocation speed, Go starts additional Goroutine to assist in marking. This diagram reflects this logic:

In our example, Goroutine 14 is aroused to work when the scan speed is lower than the allocation speed:

CPU limitation

One of the goals of the garbage collector is not to use more than 25% of the CPU. This means that Go cannot allocate more than a quarter of the processors in the marking phase. In fact, this is exactly what we saw in the previous example, where only two Goroutines are out of processor height and are dedicated entirely to garbage collection:

We can see that another Goroutine works on the tag when he has nothing else to do. However, Go can exceed 25% CPU usage at peak times when the garbage collector is making assistance requests, as seen in Goroutinue 14

In our example, 37.5 percent of the processor (three-eighths) was allocated to the marking phase for a short time. However, this may be rare and only occur in cases of high memory allocation.