[Golang] Go GC(Gargbae Collection)에 대해
최근에 면접을 봤다. 최근에 정리한 Go 1.22 Realse에 대한 정리글을 좋게 봐주셨는데, 정작 1.22의 GC에 관련된 질문에 잘 대답하지 못했다. close()만 꼼꼼히 해준다면 개발하면서 크게 신경 쓸 부분도 아니라고 생각했고, 그래서 대강 넘겼는데... 역시 항상 자만이 화를 부른다.
아무튼 이참에 외양간 한번 고쳐보면서 어렴풋이 알고 있던 부분을 확실히 하고 가려고 한다. GO GC가 언제 발생하는지, 어떻게 동작하는지, 어떤 알고리즘을 사용하는지, 어떤 특징을 가지는지 다룰 예정이다.

GC Cycle
먼저 GC의 역할에 관해 먼저 짚고 넘어가 보자. GC는 메모리의 힙(Heap)에 할당된 객체 중 더 이상 사용하지 않는 객체(Garbage)를 수거하여 Memory leak이 발생하는 것을 방지한다. 즉, 힙이 가득 차서 더 이상 힙에 객체를 할당할 수 없을때 GC는 메모리를 확보한다.
그럼 힙의 크기는 어떻게 정해질까? 고정되어 있을까? Go의 힙은 힙데이터(reachable data in the heap)의 2배 정도의 크기를 가지도록 설정되어 있다. 그러니까 1Gb의 힙데이터를 가지고 있는 Go프로세스는 2Gb정도의 메모리를 사용한다.
결과적으로 실제로 프로세스가 사용하는 1Gb와 추가적으로 확보하려는 1Gb를 합하여 2Gb의 메모리를 사용하게 되며 프로세스가 점유한 메모리가 2Gb를 넘을 때 새로운 GC사이클이 수행된다. 아래 그래프를 보자.

- live heap : 사용가능한 object이다.
- 사실 Go프로세스에서 live heap 크기의 계산은 비싼 연산이기 때문에 일정 주기로 측정된다. 하지만 아래 그래프는 연속적인 그래프로 작성되었다.
- total heap : live heap과 garbage(objects that are no longer reachable)의 총량이다.
- heap goal : total heap이 도달할 수 있는 최대치이다. 여기에 도달하면 GC를 진행한다.
이 그래프에서 live heap의 size는 증가하거나 감소하며, total heap은 GC이후에 감소하는 것을 볼 수 있다. GC사이클 종료 시 live heap의 크기(푸른 중괄호)만큼의 여유 heap(붉은 중괄호)이 heal goal로 설정된다. 결과적으로 프로세스가 사용 가능한 메모리의 크기는 GC 종료 시 live heap의 2배가 된다.
그럼 2배의 메모리 오버헤드를 발생기켜서 얻는 이점은 뭘까?
CG는 메모리를 희생함으로서 CPU오버헤드를 10% 내외로 유지하게 된다. GC는 CPU와 메모리의 오버해드의 균형을 맞추도록 설계되었으며, GC는 축적된 Garbage와 GC시 사용될 CPU 프로세스를 고려한다. 2배가 너무 많아 보이는가? 그렇다면 이 비율은 설정을 통해 바꿀 수 있다. (internal의 mgc.go를 참고하자)
- CPU와 메모리 오버헤드에 관한 내용은 이는 후속 포스팅에서 다시 다루도록 하겠다.
Tricolor mark and sweep algorithm
자 그럼 GC가 Garbage는 어떻게 찾는지 알아보자. Go GC는 Tricolor mark and sweep algorithm를 사용하여 GC 대상을 찾는다. 이 알고리즘을 간략히 정리하자면 아래와 같다. 해당 알고리즘은 말 그대로 3개의 색으로 구성된 집합을 가진다.
- White set : 프로그램에서 더 이상 접근할 수 없어서 GC 대상이 되는 객체
- Black set : 프로그램이 사용하고 있고, 흰색 집합의 객체에 대한 참조가 없는 객체.
- Grey set : 프로그램이 사용하고 있고, 흰색 집합의 객체를 가리킬 수도 있어서 검사를 진행해야 하는 객체

- GC가 시작되면 모든 객체를 흰색으로 칠하고 시작한다.
- GC가 모든 루트 객체(Root object)를 방문해서 회색으로 칠한다.
- 루트 객체란, 스택이나 전역 변수처럼 애플리케이션에서 직접 접근할 수 있는 오브젝트를 말한다. 위 예제에서는 A, F가 회색으로 색칠되었다.
- GC는 회색 집합의 객체를 하나씩 검은색으로 변경하며 그 객체를 참조하는 객체(B, C, D)를 회색으로 칠한다.
- 앞선 작업을 회색 집합에 객체가 없어질 때까지 반복한다. 이 작업이 끝나면 더 이상 접근할 수 없는 객체만 희색 집합에 남는다.
- 흰색 집합의 객체에 할당된 메모리 공간을 회수한다.
Metadata
좋다. 이제 어떻게 Garbage를 찾는지도 알았다. 그럼 어떻게 판단하는지 알아보자.
Function Metadata는 go의 Runtime package에 _func struct로 선언되어 있다. 이 struct는 함수에 관련된 정보를 가지고, 함수가 동작할 때 이용된다.
type _func struct {
sys.NotInHeap // Only in static data
entryOff uint32 // start pc, as offset from moduledata.text/pcHeader.textStart
nameOff int32 // function name, as index into moduledata.funcnametab.
args int32 // in/out args size
deferreturn uint32 // offset of start of a deferreturn call instruction from entry, if any.
pcsp uint32
pcfile uint32
pcln uint32
npcdata uint32
cuOffset uint32 // runtime.cutab offset of this function's CU
startLine int32 // line number of start of function (func keyword/TEXT directive)
funcID abi.FuncID // set for certain special runtime functions
flag abi.FuncFlag
_ [1]byte // pad
nfuncdata uint8 // must be last, must end on a uint32-aligned boundary
}
예를 들어 panic이 발생하면 어셈블리 명령의 프로그램 카운터는 이 메타데이터를 이용해 현재 파일과 라인 번호, 그리고 stack trace 전부를 얻는다. 파일과 라인 번호는 pcfile 과 pcln 필드를 이용하고, stack trace는 pcsp 를 이용한다.
GC도 마찬가지로 이 Metadata를 이용한다. 런타임이 GC을 실행할때 stack에 저장된 값이 포인터인지 아닌지 타입을 파악해야 하는데 이때 funcdata를 이용한다.
컴파일러는 각 함수마다 2개의 변수를 만든다. 각각의 변수는 stack과 관련된 bitmap vector와, 함수 내부에 선언된 변수들과 관련된 bitmap vector를 포함한다. 그럼 GC는 이 정보들을 가지고 참조를 확인하여 GC할 대상을 선별하는 작업을 수행한다.
참고로 Go 1.22에서 GC쪽 개선사항이 있다. Reales note의 runtime을 보면 Type기반의 GC Metadata를 Heap에 더 가깝게 유지하여 CPU의 성능을 개선했다고 한다.
The runtime now keeps type-based garbage collection metadata nearer to each heap object, improving the CPU performance (latency or throughput) of Go programs by 1–3%.
본 포스팅을 통해 알아본 내용은 아래와 같다.
- Go Process는 CG사이클이 종료될때 사용하는 힙 데이터만큼의 여유분을 확보한다.
- 이때 확보한 힙이 가득 차면 새로운 GC사이클이 수행된다.
- GC는 Tricolor mark and sweep algorithm를 사용하여 Garbage를 찾는다.
- 이때 function metadata를 가지고 Garbage인지 아닌지 판단한다.
참고한 글들
- https://go.dev/blog/ismmkeynote
- https://tip.golang.org/doc/gc-guide
- https://golangkorea.github.io/post/golang-internals/part4/
- https://www.altoros.com/blog/golang-internals-part-4-object-files-and-function-metadata/
- https://www.samsara.com/blog/reducing-costs-for-large-caches-in-go/
- https://velog.io/@kineo2k/Go-%EC%96%B8%EC%96%B4%EC%9D%98-GC
- 학교다닐때 전공서적....