Go의 GC는 mark-sweep 방식으로 동작하며 두 가지 주요 단계로 나뉜다.
- 마킹: 메모리에서 살아있는 객체를 식별한다.
- 스윕: 죽은 객체를 해제하여 메모리를 반환한다.
GC는 스윕 → 비활성 → 마킹 세 단계를 순회하며 주기적으로 진행된다.
Go의 GC는 현시점의 Heap 크기와 직전 시점의 Heap 크기에 대한 증가율이 특정 정도에 다다르면 수행되는 로직으로 구현되었다. 이 때 그 정도를 정하는 변수는 GOGC라고 부른다. GOGC 값을 사용해 아래와 같이 목표 힙 메모리를 구한다.
목표 힙 메모리 = 살아있는 힙 메모리 + (살아있는 힙 메모리 + GC 루트) × GOGC / 100
예를 들어, 살아있는 힙이 8 MiB이고, GOGC가 100일 때, 목표 힙 메모리는 18 MiB이다. GOGC가 50일 경우 13 MiB, GOGC가 200일 경우 28 MiB가 된다.
GC가 동작하는 Default 비율 값은 100으로, 기존 대비 Heap이 100% 증가, 즉 2배가 되면 GC를 수행한다. 이 값을 낮추면 GC가 더 자주 수행되고, 이 값을 높일수록 GC가 덜 수행되게 된다. GOGC 값이 너무 작은 경우 GC가 너무 빈번하게 수행될 수 있고, GOGC 값이 너무 큰 경우 OOM 발생 가능성이 커진다.
GOMEMLIMIT은 프로그램이 사용할 수 있는 메모리 사용량 한계선을 정하는 설정이다. 이 방식에서는 설정한 GOMOMLIMIT의 값만큼 메모리 사용량이 올라가는 경우에만 GC가 수행된다. 따라서 프로그램이 사용할 수 있는 최대 메모리 한계선을 미리 산정한 후, 그 값보다 작은 값을 GOMEMLIMIT으로 설정하면 GC를 쉽게 튜닝할 수 있다.
Heap 할당을 줄이는 팁
비정형 인자 조심하기
아래 코드에서 j와 k 객체는 Heap 메모리에 할당되지만, i는 Stack 메모리에 할당된다.
fmt.Println
, fmt.Printf
함수는 두 함수는 any 타입의 인자를 사용하기 때문에 무엇이든 전달받을 수 있다. 그리고 무엇이든 받기 위해, 각 변수에는 그 type을 설명하는 보조 정보가 필요하다. 이때, 보조 정보가 있는 데이터는 반드시 Heap 메모리 영역에 할당된다. 즉 any 만이 아니라, interface 및 reflect 등의 타입을 읽고 해석하는 추상화가 필요한 데이터들은 GC 부하를 일으키는 원인으로 작용한다.
Pointer 변수 조심하기
Go에서 포인터로 선언하면, 해당 오브젝트는 무조건 Heap 영역에 할당된다. 따라서 Go에서는 CallByPointer보다 CallByValue가 효율적인 경우가 종종 있다.
포인터 배열
go에서 GC시에 포인터가 일반 값보다 오래걸린다.
https://blog.gopheracademy.com/advent-2018/avoid-gc-overhead-large-heaps
참고