Go의 가비지 컬렉션(GC)은 언어의 초기 버전에서 처음 등장한 자동 메모리 관리 메커니즘입니다. 역사적으로 Go의 GC는 성능에 미치는 영향 때문에 비판의 대상이 되었습니다. 하지만 언어가 발전하면서, 특히 Go 1.5 버전 이후로, GC는 크게 개선되었습니다: 현재는 최소한의 정지 시간을 가지는 삼중 동시(multi-threaded, tricolor, mark-and-sweep) 가비지 수집기가 사용됩니다.
문제는 프로그램이 많은 임시 객체를 생성하거나 사용되지 않는 구조체에 대한 참조를 삭제하지 않을 때 발생합니다: 이는 GC에 부담을 주고 긴 정지 시간을 초래할 수 있습니다. 객체의 유형, 순환 참조 및 스택 밖에 있는 긴 참조 체인에 주의해야 합니다.
해결책은 메모리 할당을 모니터링하고, 프로파일링하며, 환경 변수 GOGC를 통해 GC 튜닝을 수행하고, 내부 루프 및 크리티컬 섹션에서 할당 수를 최소화하는 것입니다. Go의 가비지 수집기는 힙(Heap)에서만 작동하므로 스택에서 할당된 모든 것은 범위를 벗어날 때 자동으로 제거되며, 힙으로 "간" 객체는 GC에 의해 관리됩니다.
코드 예시:
// 메모리 할당 프로파일링 및 GC 최적화 import ( "runtime" "fmt" ) func main() { var memStats runtime.MemStats runtime.ReadMemStats(&memStats) fmt.Printf("할당 전: %d bytes ", memStats.Alloc) s := make([]int, 1_000_000) for i := range s { s[i] = i } runtime.GC() // 수동 정리 runtime.ReadMemStats(&memStats) fmt.Printf("GC 후: %d bytes ", memStats.Alloc) }
핵심 특성:
GOGC를 통해 조정할 수 있습니다(예: GOGC=100 — 기본값; 이를 줄이면 GC가 빨라지지만 CPU 소비가 증가합니다).어떤 객체가 힙으로 "가고", 어떤 객체가 스택에 남는지 어떻게 알 수 있나요?
답변: 이를 위해서는 escape analysis를 사용하며, 이는 컴파일러 플래그 go build -gcflags="-m"을 통해 분석할 수 있습니다. 함수 외부로 반환되거나 클로저에서 사용되는 객체는 대개 힙으로 이동합니다.
코드 예시:
func escape() *int { v := 42 return &v // v는 힙에 있을 것 }
GC는 스택의 변수를 포함하여 모든 변수에 영향을 미쳐야 하나요?
아니요, GC는 힙에서 할당된 객체만 작동합니다. 스택에서 할당된 것은 함수가 종료되면 자동으로 정리됩니다.
수동으로 runtime.GC()를 호출하면 성능이 크게 향상될 수 있나요?
반대로, 수동 호출은 종종 성능을 저하시켜 CPU 소비를 증가시킵니다. 테스트나 디버깅 상황에서만 사용하는 것이 좋습니다.
부정적인 사례
개발자가 각 요청마다 새로운 큰 슬라이스를 생성하는 서비스를 작성할 때, 그들의 생명주기를 고려하지 않습니다. 이는 GC에 대한 부담 증가, 긴 정지 시간 및 성능 저하로 이어집니다.
장점:
단점:
긍정적인 사례
개발자가 서비스를 최적화하고 메모리 할당을 프로파일링하며, sync.Pool을 통해 버퍼를 재사용하고 GC 호출 빈도를 줄이며, 핫스팟에서의 메모리 할당을 최소화합니다.
장점:
단점: