메모리 관리는 프로그램의 성능과 안정성에 직접적인 영향을 미칩니다. 수동 메모리 관리 방식과 자동 메모리 관리 방식은 각기 다른 장단점을 가집니다.Go는 자동 메모리 관리를 채택하여 개발 편의성을 높였습니다.
수동 메모리 관리의 한계
C와 같은 언어에서는 프로그래머가 malloc이나 calloc으로 메모리를 할당하고, free 함수를 호출하여 명시적으로 해제해야 합니다. 이는 메모리에 대한 높은 제어권을 제공하지만, 다음과 같은 두 가지 주요 프로그래밍 오류를 유발할 수 있습니다.
-
댕글링 포인터(Dangling Pointer): 메모리가 너무 일찍 해제되어 포인터가 유효하지 않은 메모리 위치를 가리키는 경우 발생합니다. 이는 예측 불가능한 프로그램 동작으로 이어집니다.
-
메모리 누수(Memory Leak): 사용하지 않는 객체의 메모리를 해제하지 않아 메모리가 점차 고갈되는 현상입니다. 이는 프로그램 속도 저하 또는 충돌을 야기할 수 있습니다.
자동 메모리 관리 (가비지 컬렉션)
Go는 이러한 수동 관리의 위험을 피하기 위해 가비지 컬렉션을 통한 자동 동적 메모리 관리를 제공합니다. 가비지 컬렉션은 보안 강화, 운영체제 간 이식성 향상, 코드량 감소, 런타임 검증 등의 이점을 제공하며, 개발자가 메모리 관리 걱정 없이 핵심 비즈니스 로직에 집중할 수 있도록 돕습니다.
스택(Stack)과 힙(Heap)
프로그램은 객체를 스택과 힙이라는 두 가지 메모리 위치에 저장합니다. 가비지 컬렉션은 주로 힙 메모리를 대상으로 작동합니다.
-
스택: LIFO(Last-In, First-Out) 구조로, 함수 호출 시 생성되는 스택 프레임과 지역 변수를 저장합니다. Go는 고루틴(goroutine)당 스택을 가지며, 컴파일러의 이스케이프 분석(escape analysis)을 통해 생명주기가 명확한 변수는 스택에 할당됩니다.
-
힙: 함수 외부에서 참조되거나 생명주기가 불분명한 객체(예: Go 구조체)를 저장합니다. 힙에 할당된 객체는 포인터로 참조되며, 프로그램 실행 중 계속해서 증가할 수 있습니다.
Go의 가비지 컬렉터 구현
Go의 가비지 컬렉터는 “비세대(non-generational) 동시(concurrent) 삼색(tri-color) 마크 앤 스윕(mark and sweep)” 방식으로 작동합니다.
-
비세대(Non-generational): Go 컴파일러는 이스케이프 분석을 통해 단명하는 객체를 스택에 할당하므로, 힙에는 장명 객체가 많아 세대별 GC의 효율이 낮아 비세대 방식을 채택합니다.
-
동시(Concurrent): 가비지 컬렉터가 애플리케이션 코드(뮤테이터 스레드)와 동시에 실행되어 프로그램 중단 시간(Stop the World)을 최소화합니다.
-
마크 앤 스윕(Mark and Sweep): 두 단계로 구성됩니다.
- 마크(Mark) 단계: 컬렉터가 힙을 탐색하여 사용 중인 객체(라이브 객체)를 표시합니다. Go는 삼색 알고리즘을 사용합니다.
- Stop the World: GC 시작 시 모든 고루틴을 일시 중지하고 쓰기 장벽(write barrier)을 활성화하여 데이터 무결성을 보장합니다.
- 삼색 알고리즘: 모든 객체를 흰색(white)으로 초기화하고, 루트 객체(스택, 전역 변수, 힙 포인터)를 회색(grey)으로 표시합니다. 워커는 회색 객체를 스캔하여 참조하는 객체를 회색으로 만들고 자신은 검은색(black)으로 변경합니다. 검은색은 해당 객체가 사용 중이며 모든 참조가 스캔되었음을 의미합니다.
- 스윕(Sweep) 단계: 마크 단계가 완료된 후, 다시 Stop the World를 수행하고 흰색으로 남아있는(더 이상 참조되지 않는) 객체들의 메모리를 해제합니다.
- 마크(Mark) 단계: 컬렉터가 힙을 탐색하여 사용 중인 객체(라이브 객체)를 표시합니다. Go는 삼색 알고리즘을 사용합니다.
GC 트리거
Go의 가비지 컬렉션은 현재 사용 중인 메모리에 비례하여 추가 메모리가 할당될 때 다시 시작됩니다. GOGC 환경 변수(기본값 100)를 통해 이 비율을 조절할 수 있으며, 이는 GC 비용이 할당 비용에 비례하도록 유지합니다.