GC, Garbage Collector
Garbage Collector(GC)은 프로그램 실행 중 사용되지 않는 메모리를 자동으로 회수하는 메모리 관리 기법이다.
이를 통해 개발자는 명시적으로 메모리를 해제할 필요 없이, 안전하고 효율적인 메모리 관리를 수행할 수 있습니다.
Go에서는 이런 GC를 Concurrent Mark and Sweep(CMS) 방식을 채택하여 애플리케이션과 GC 작업을 동시에 수행합니다, Go는 STW의 Latency 시간을 최소화하는 방향으로 발전해 왔습니다.
Golang GC 튜닝의 핵심은 STW가 덜 발생하도록, 발생해도 빨리 해소되도록 하는 게 목표입니다.
Concurrent Mark and Sweep, CMS
CMS는 Tri-color Marking 기법으로 GC 작업을 수행합니다.
기본 원리는 객체를 3가지 색상으로 구분하여 GC를 작업합니다.
색상 | 설명 |
Black | 사용 중인 객체로 하위 모든 참조가 마킹 완료됨 (GC로 해제하면 안됨) |
Grey | 루트에서 접근 가능하지만, 아직 모든 자식 객체가 마킹되지 않음 |
White | GC 대상 객체, 마킹되지 않은 객체로, 최종적으로 해제됨 |
위 색상을 기준으로 아래 순서로 GC 작업을 진행합니다
1. 초기 상태
- GC 시작 전, 모든 객체를 White 상태로 간주
2. Marking 단계 (동시 실행)
- 루트 객체(전역 변수, 스택 변수 등)에서 접근 가능한 객체는 Grey로 상태 변경
- Grey 객체를 하나씩 탐색하며 Black로 상태 변경
- Grey 객체의 탐색이 완료되면 Black 상태로 변경
- Grey 객체가 더 이상 없을 때까지 반복
3. Sweep 단계 (White 객체 해제)
- Black 객체에서 White 객체로의 경로가 없다면, White 객체는 사용되지 않는 걸로 판단하여 메모리에서 해제함
4. GC 종료
- 모든 객체가 Black 또는 해제된 상태(White 삭제)가 되면 GC는 종료
※ Write Barrier 역할
GC의 CMS을 수행할 때 애플리케이션 고루틴이랑 같이 실행되므로 언제든지 새로운 객체 할당이나 참조 변경이 발생할 수 있습니다.
이때 Go는 Write Barrier를 사용하여 GC 수행 중 객체 참조가 변경될 경우 즉시 Grey 상태로 등록해서 (Read Barrier 대신 이걸 사용) 해결합니다.
Stop-the-World, STW
STW는 GC가 안전한 메모리 회수를 위해, 애플리케이션의 모든 고루틴을 일시적으로 중지시키는 상태를 의미합니다.
STW 시간에 영향을 미치는 요인
- 고루틴의 상태
GC는 고루틴이 함수 호출 시 안전하게 중지할 수 있습니다.
그러나 함수 호출이 없는 타이트 루프(tight loop)에서 실행 중인 고루틴은 중지 신호를 받지 못해 STW 시간이 길어질 수 있습니다. (이미지와 같은 대용량 데이터를 처리할 때 주로 발생)
- 힙 크기 및 객체 수
힙 메모리의 크기와 객체의 수가 많을수록 GC 작업량이 증가하여 STW 시간이 늘어날 수 있습니다.
STW 지연 시간 최적화 방안
- 메모리 할당 최적화
불필요한 메모리 할당을 줄이고, 가능한 한 객체를 재사용하여 힙 메모리 사용을 최소화
- 고루틴 설계 개선:
타이트 루프를 피하고, 주기적으로 함수 호출이나 runtime.Gosched()를 사용하여 고루틴이 중지 신호를 받을 수 있도록 설계합니다.
※ 타이트 루프(Tight Loop) : 매우 짧은 코드로 반복 실행되는 루프로, 무한에 가깝거나 실행시간이 길어서 CPU를 독점적으로 사용하여 다른 프로세스에 방해를 줄 수 있는 루프, GC 및 고루틴 스케줄링이 정상적으로 동작하지 않을 수 있음
- GC 튜닝
GOGC 환경 변수를 조절하여 GC의 빈도를 설정할 수 있습니다. 기본값은 100이입니다.
이는 힙 크기가 두 배(100%)로 증가할 때마다 GC가 실행됨을 의미합니다.
이는 공식 블로그에서 GOGC 크기 별 시뮬레이션이 가능합니다.
https://tip.golang.org/doc/gc-guide?utm_source=chatgpt.com
GC 대상 객체
이때 지우는 객체들은 Heap에 저장된 변수들로 다음 코드와 같이 New나 Make, 전역변수들을 의미합니다.
GC는 Heap에 저장된 데이터만을 대상으로 합니다.
Stack에 저장되는 지역변수나 매개변수는 함수가 끝나면 자동으로 해제가 되기 때문입니다.
즉 아래 코드와 같이 New를 쓰거나 Make 또는 전역변수일 경우만 GC가 적용됩니다.
func heapExample() *int {
x := new(int) // 힙에 저장
*x = 42
return x // 함수 종료 후에도 x는 살아있음
}
func main() {
p := heapExample() // p가 x를 참조하고 있음
fmt.Println(*p) // 42 출력
} // p가 사라지면 GC가 x를 해제함
Go 컴파일러는 Escape Analysis를 통해 변수의 할당 위치를 스택으로 할지, 힙으로 하지를 자동으로 결정합니다.
Escape Analysis
Go 컴파일러는 Escape Analysis를 통해 변수의 할당 위치(스택 vs 힙)를 자동으로 결정
- 변수가 함수 안에서만 사용되면 스택에 할당
- 함수 밖에서도 사용되면 힙에 할당
- GC 부하를 줄이기 위해 가능한 한 스택에 할당하려고 시도
package main
import "fmt"
func escapeExample() *int {
x := 42
return &x // x는 힙에 할당됨 (Escape)
}
func noEscapeExample() {
x := 42 // 스택에 저장
fmt.Println(x)
}
func main() {
fmt.Println(*escapeExample())
noEscapeExample()
}
위 예제를 아래 옵션으로 실행하면 Escape Analysis 결과를 확인할 수 있음
go run -gcflags "-m" main.go
# 결과물
# command-line-arguments
./main.go:5:6: can inline escapeExample
./main.go:12:13: inlining call to fmt.Println
./main.go:16:28: inlining call to escapeExample
./main.go:16:13: inlining call to fmt.Println
./main.go:6:2: moved to heap: x
./main.go:12:13: ... argument does not escape
./main.go:12:14: x escapes to heap
./main.go:16:13: ... argument does not escape
./main.go:16:14: *(~R0) escapes to heap
42
42
번외로, Slice에 경우 기본적으로 Heap에 저장되지만
컴파일러의 최적화 여부에 따라 stack에도 저장될 수 있습니다.
s := make([]int, 5, 10) // len, cap 지정
위와 같이 slice를 지정할 경우 escape 분석을 통해 stack으로 충분히 처리가 가능하다면 stack에 저장이 되고
하지만 이와 같은 코드여도 선언된 함수 밖에서 참조하면 escape가 발생하여 heap에 저장됩니다.
GC Memory
GoLang은 No Compactiong을 합니다.
즉 메모리 단편화 현상이 발생하더라도, 한 곳으로 모으는 기법을 사용하지 않는다는 것이고 이게 Java와의 차이점이죠.
아래 이미지는 Compacting을 설명한 이미지로, 객체를 다시 읽고 씀으로써 메모리의 단편화를 없앱니다.
이런 Compaction을 Go의 GC에서 지원하지 않는 이유는 아래와 같습니다.
Go의 철학을 지키위 함이 가장 큽니다
- 단순성 유지
- Compacting을 하기 위해서는 Read/Write Barrier를 둘 다 필요하지만 Go는 성능을 위해 Read Barrier을 추가 안 했습니다.
- 고루틴 스케줄러와 충돌 방지
- Compacting 과정 중에 생기는 주소 변경이 고루틴이 사용 중인 객체를 참조할 경우 충돌이 발생할 수 있음
- 이를 해결하려면 STW 시간이 길어지는데 이는 Go의 철학에 위배됨
- TCMalloc을 이용한 단편화 방지
- Go는 Compacting 대신 TCMalloc 기반의 메모리 할당 방식으로 메모리 단편화를 줄이는 전략을 선택함
TCMalloc (Thread-Caching Malloc)
TCMalloc이란?
구글에서 개방한 메모리 할당 기법으로, Go의 메모리 할당자는 이것과 유사한 방식을 사용하여 빠르고 효율적인 메모리 관리를 수행
TC Malloc 특징
- 작은 객체의 빠른 할당
- 작은 객체(<=32KB)를 위한 스레드 로컬 캐싱(Thread-local caching) 을 사용
- 고루틴마다 자체적으로 메모리를 관리하여, 경합(Contention)을 줄이고 빠른 할당이 가능
- 대형 객체는 전역 할당 사용
- 큰 객체(32KB 이상)는 전역 힙(Global Heap)에서 관리됨.
- 프리 리스트(Free List) 사용
- 메모리 해제 시, 바로 반환하지 않고 다시 사용하도록 캐싱하여 성능 최적화.
Go에서의 TCMalloc
Go의 메모리 관리자는 TCMalloc과 유사한 방식으로 동작하며, 이를 통해 단편화를 줄이고 GC 부담을 감소시킴
Google에서 만들었으니 Google 기술을 사용하는군요
참고 문서
- GoLang Doc(Gc Guid) : https://tip.golang.org/doc/gc-guide?utm_source=chatgpt.com
- Kakao tech Golang GC 튜닝 가이드 : https://tech.kakao.com/posts/618?utm_source=chatgpt.com
- 2023 고퍼콘 GC : https://www.youtube.com/watch?v=EVBFXZhS07E&ab_channel=GolangKorea
'Go Lang > Study' 카테고리의 다른 글
[GoLang] net/http에서 http 요청을 동시적으로 처리하는 이유 (0) | 2025.02.17 |
---|---|
[GoLang] GC 튜닝 Profiling (0) | 2025.02.05 |
[GoLang] 추상 팩토리 디자인 사용해보기 (1) | 2025.01.17 |
[GoLang] GraphQL API 만들기 part 1 (라이브러리 탐색) (38) | 2024.03.01 |
[GoLang] GoLang 면접 질문 정리 (2) | 2024.02.26 |