-
Go는 힙에서 동적 할당이 필요한 메모리를 암묵적으로 할당한다.
- 할당이 암묵적으로 이뤄지기 때문에 코딩이 쉽지만, 메모리 할당과 해제가 명확하지 않으니 메모리 사용량이 높아질 수 있는 부분을 놓칠 가능성이 있다.
-
Go는 참조 지향이 아닌 값 지향적 언어이다. Go 변수는 절대 객체를 참조하지 않고, 항상 객체의 전체 값을 저장한다. (포인터가 없다는 의미는 아니다.)
- 모든 변수 선언(함수 인자, 반환 인자, 메서드 리시버 포함)은 해당 타입 전체를 할당하거나 그 포인터만 할당한다.
-
new(<type>)
은&<type>
과 동일하며, 힙 상의 포인터 박스와 별도의 메모리 블록에 타입을 할당한다. -
Go 할당자는 특정 스팬에 하나 혹은 여러 8KB 페이지를 포함하는 메모리 블록을 할당하여 단편화를 해결한다.
- 각 스팬을 클래스 메모리 블록 크기에 맞게 생성된다.
- Go 1.21에서는 67개의 크기 클래스가 있으며 32KB가 최대 크기다.
-
Go 할당자는 가상 메모리 영역에서 메모리 블록을 bin packing한다. 또한 0으로 초기화된 개인 익명 페이지와 함꼐
mmap
을 사용하여 운영체제로부터 더 많은 공간을 요청한다. -
하지만 메모리 공간을
make
로 할당받는 즉시 사용할 수 있는 메모리 공간을 받는 것은 아니다. 600MB의 바이트 슬라이스를 사용하는 예제를 살펴보자.
- b 변수는
[]byte
슬라이스로 선언된다. 이후 등장하는make
문은 600MB 크기의 데이터를 가진 바이트 배열을 만들어 힙을 할당하는 작업이다.- 이를 디버깅해보면 아래와 같은 정보를 알 수 있다.
- 해당 슬라이스에 사용되는 세 가지 메모리 매핑에 대한 RSS는 548KB, 0KB, 120KB이다. (VSS 번호보다 훨씬 작음)
- 전체 프로세스의 총 RSS는 21MB이며, 프로파일링에 따르면 대부분 힙의 외부에서 들어오는 것으로 나타났다.
- 힙 크기는 600.16MB이다. (RSS가 훨씬 더 낮음에도 불구하고)
- 슬라이스 요소에 대한 액세스(쓰기 또는 읽기)를 시작하면 운영체제는 해당 요소를 둘러싼 실제 물리 메모리를 예약하기 시작한다.
- 3개의 메모리 매핑 RSS는 556KB, 0KB, 180KB이다.
- 총 RSS는 여전히 21MB이다.
- 힙 크기는 600.16MB이다. (실제로는 더 크지만 배경이나 루틴 때문일 수 있다.)
- 모든 요소에 반복해서 접근한 후, b 슬라이스의 모든 페이지가 요구에 따라 피지컬 메모리에 매핑되는 것을 볼 수 있다.
- 다음 통계가 이를 증명한다.
- 3개의 메모리 매핑에 대한 RSS는 1.5MB, (완전히 매핑된) 598MB, 그리고 1.2MB이다.
- 전체 프로세스의 RSS는 621.7MB를 나타낸다. (마침내 힙 사이즈와 같아졌다.)
- 힙 크기는 600.16MB이다.
가비지 컬렉션
-
Go는 주기적으로 힙에 있는 객체를 대상으로 가비지 컬렉션을 실행한다.
-
GOGC
옵션은 가비지 컬렉터 비율을 나타낸다.- 기본값은 100이다.
- 가비지 컬렉터 주기가 끝난 후 힙 크기가 n%가 될 떄 수행될 것이라는 의미다.
- debug.SetGCPercent 함수를 사용해서 프로그래밍적으로 설정할 수도 있다.
-
GOMEMLINIT
옵션은 소프트 메모리 제한을 제어한다.- 기본값은 비활성화되어 있으며 설정된 메모리 제한에 가까운 경우에 가비지 콜렉터를 더 자주 실행하도록 한다.
-
runtime.GC()
를 호출하여 가비지 컬렉터의 수집을 트리거할 수도 있다.- GC 구현은 여러 단계로 구정된다.
- STW(Stop the world) 이벤트를 실행하여 모든 고루틴에 Write barrier(데이터 쓰기에 대한 lock)을 주입한다.
- 프로세스에 제공된 CPU 용량의 25%를 사용해, 쓰고 있는 힙의 모든 객체를 표시한다.
- 고루틴에서 쓰기 장벽을 제거하여 표시를 종료한다.
- GC 구현은 여러 단계로 구정된다.
-
Go 런타임은 가비지 컬렉터 실행시
MADV_DONTNEED
인수를 사용하여madvise
시스템 콜을 사용한다.- 그렇기 떄문에 호출 프로세스의 RSS는 즉시 감소해도, 페이지는 즉시 해제되지 않고 커널이 적절한 순간까지 지연시킬 수도 있다.
-
자세한 동작을 이해하기 위해 600MB의 바이트 슬라이스를 GC하는 예제를 살펴보자.
-
큰 슬라이스를 할당하고 모든 요소에 액세스한 후의 통계는 다음과 같다.
- 3개의 메모리 매핑에 대한 RSS는 1.5MB, 598MB, 그리고 1.2MB이다.
- 전체 프로세스의 RSS는 621.7MB를 나타낸다.
- 힙 크기는 600.16MB이다.
-
b = nil
로 초기화하고 GC를 수동 호출한 후의 동계는 다음과 같다.- 세 가지 메모리 매핑에 대한 RSS는 1.5MB, 0, 60kb이다. (중간 값이 해제되었고, VSS 값은 동일하다.)
- 전체 프로세스의 총 RSS는 21MB이다.
- 힙 크기는 159KB이다.
-
더 작은 슬라이스를 하나 더 배정하면 이전 메모리 매핑을 다시 사용한다.
- 세 가지 메모리 매핑에 대한 RSS는 1.5MB, 300MB, 60KB다.
- 전체 프로세스의 총 RSS는 321MB다.
- 힙크기는 300.1KB이다.
참고