Garbage Collection in GO
Aagosh Aggarwal�Backend Engineer�Merwork Team
1
Types of Memory
2
Which variable goes where?
3
Escape Analysis
Go does escape analysis at compile time to figure out which variables should be allocated on the stack and which ones should escape to the Heap.
�From the Golang FAQ:�When possible, the Go compilers will allocate variables that are local to a function in that function's stack frame. However, if the compiler cannot prove that the variable is not referenced after the function returns, then the compiler must allocate the variable on the garbage-collected heap to avoid dangling pointer errors. Also, if a local variable is very large, it might make more sense to store it on the heap rather than the stack.
4
Stack Allocation
package main func main() { n := 1 n = increment(n) println(n) } func increment(x int) int { return x+1 } |
5
|
|
|
func main()��n = 1
package main func main() { n := 1 n = increment(n) println(n) } func increment(x int) int { return x+1 } |
func increment()
x = 2
func main()
n = 2
func main()�n = 2
func println()�n =2
func increment()
x = 2
func increment()
x = 2
Here the stack frames marked red are invalid as the function has returned. Go reclaims these invalid stack frames itself.
6
Heap Allocation
package main func main() { n := allocate() *n++ println(*n) } func allocate() *int { x := 1 return &x } |
7
|
|
func allocate()��x = 1
package main func main() { n := allocate() *n++ println(*n) } func allocate() *int { x := 1 return &x } |
func main()
n = address of X
func main()
n = address of X
func allocate()
x = 1
╳
This will lead to Dangling pointer error
8
|
func allocate()�
package main func main() { n := allocate() *n++ println(*n) } func allocate() *int { x := 1 return &x } |
func main()
n = address of X
HEAP�x = 1
9
Garbage Collection
10
Go’s Garbage Collector
Go uses a non-generational, non-compacting, concurrent, tricolor mark and sweep Garbage collector.
�Non-Generational: Generational GC relies on generational hypothesis: most allocated values are unused quickly, so it is better for the GC to run through the recently allocated objects first. �Because GO uses escape analysis and tries to allocate objects with short life-span on the stack, so it does not really need a Generational Garbage collector.
Non-Compacting: Compacting GC ensures that the spaces between the objects in heap are compacted. This way the heap memory remains contiguous. The allocator used by Go, tcmalloc, reduces fragmentation by itself and so it does not use a compacting GC as of now.
Concurrent: GO’s garbage collector runs concurrently with the application processes. It does not STW (Stop the world) during the entire collection process.
�Tricolor Mark and Sweep: This is the process through which collection takes place.
11
Phases of Garbage Collection
12
Mark Setup
GC Collection Starts |
Stop the World |
Write Barrier turned ON |
Start the World |
13
Stop the World
M
M
M
M
P1
P4
P3
P2
G
G
G
G
14
Marking
15
Marking
Image source: https://developer.com
16
Marking
M
M
M
M
P1
P4
P3
P2
GC
G
G
G
17
Mark Assist
Goroutines are involved in the collection process.
18
Mark Assist
M
M
M
M
P1
P4
P3
P2
GC
MA
G
G
19
Mark Termination
Stop the World
Write barriers turned off
Start the World
Next GC Goal calculated
20
Sweeping
21
Pacing
22
GC Percentage
If heap memory in use after a GC cycle was 10 MB and the GC percentage is 100%, that means that the next GC will be triggered when the heap memory would be 20 MB.
Next Garbage collection initiated when the heap memory is 20 MB.
10 MB
20 MB
23
Writing GC Friendly Code
func allocateSlice() []int {
slice1 := make([]int, 10000000)
for i:=0; i<len(slice1); i++ {
slice1[i] = i
}
// slice2 uses the same underlying array as slice1
slice2 := slice1[1:3]
return slice2
}
func main() {
slice := allocateSlice()
runtime.GC()
slice = append(slice, 5)
runtime.GC()
}
Although we have sliced this, the underlying array wont be garbage collected as the same array is still being referenced.
The underlying array does not get GC here.
The previous underlying array gets GC here as after append the underlying array changes
24
Writing GC Friendly Code
type Reader interface {
Read(p []byte) (n int, err error)
}
io.Reader interface is implemented as above. It would have been clearer if it would have returned the slice read like below:
type Reader interface {
Read(n int) (p []byte, err error)
}
Reason: This way the byte slice is allocated on the stack. If it would have been implemented like the second alternative, the byte slice would be allocated on heap and would have resulted in a lot of garbage on the heap.
25
Conclusion
26