뽀미의 개발노트

Go 포인터와 GC 본문

Go lang

Go 포인터와 GC

산타는 뽀미 2024. 12. 20. 23:13

 

Go의 GC

 

 


 

 

포인터와 GC(Garbage Collector)

Go는 포인터를 제공하면서도, GC를 사용함. 개발자가 포인터로 메모리를 유연하게 관리할 수 있고, 안전하게 자동으로 메모리 관리도 해준다. 포인터 + GC 성능과 안전성 모두 잡는 것.

포인터

C/C++ 같은 언어에서는 포인터를 제공함. 포인터를 사용하면 메모리 영역에 직접 접근하여 인스턴스를 조작할 수 있음. 메모리를 중복으로 복사하여 할당하는 경우를 줄이고 불필요한 메모리 낭비를 막을 수 있음. 그러나 잘못 사용하면 메모리 누수, 해제된 메모리 참조, 버그 발생 등의 위험이 있어 메모리 관리를 전적으로 개발자가 책임져야 함. Go에서는 포인터를 제공하지만 포인터 연산은 허용하지 않음. 주로 참조 전달이나 struct 내 메모리 참조를 위해 사용되며, GC는 이를 정리함.

GC

GC는 사용하지 않는 객체나 메모리를 자동으로 해제하여 메모리 누수를 방지함. Java, Go, C#, Python 등은 GC를 활용하여 메모리 관리를 자동화함. 개발자가 메모리 할당에 대해 고민할 필요가 없음. 포인터가 있더라도 GC가 자동으로 사용 여부를 판단하여 포인터의 사용이 메모리 관리 오류로 연결되지 않도록 보장함.



 





Go의 GC 특이점

고를 사용하는 개발자들 중 Go의 GC가 문제라고 하는 의견이 많아 GC를 알아봄. GC는 알아서 메모리를 관리해주니 무조건 좋을 거라고 생각하면 안됨. GC하기 위해 객체를 스캔하면서 CPU를 소모하기 때문에 되려 부담을 주기도 함. Go의 GC는 Tricolor mark and sweep algorithm (삼색 표시 후 쓸어 담기 알고리즘)을 사용하여 메모리 해제함. 

모든 객체를 흰색으로 칠한 후 시작함. Root 객체를 방문해서 회색으로 칠함. 걔를 검은색으로 칠하고 그 객체가 가리키는 흰색 객체 검사.

흰색 - 검사 전 / 회색 - 검사 중 / 검은색 - 검사 끝

이라고 보면 됨. 암튼 그래서 마지막 까지 흰색으로 남아있는 E, G, H 이런 객체에 할당된 메모리 공간을 회수해서 메모리를 확보함.

그런데 우리가 작성한 코드를 진행하다가 Mutator threads가 힙의 객체 참조를 변경하면?? → 그러면 GC가 어떤 객체의 메모리를 해제해야 할지 헷갈리게 됨!!! 그래서 객체의 정합성을 유지하기 위해 GC는 Mutator threads를 중지시킴. 이 상태를 Stop the world(STW)라고 부르고, 이것 때문에 성능 저하가 일어날 수 있음. GC 하느라 Latency(지연 시간)이 발생하기도 하는데, 이걸 막기 위해 GC를 동시에 수행하는 Concurrent Mark and Sweep(CMS)를 수행함. 근데 CMS를 하면 메모리 할당 속도가 느려질 수 있음.

Go가 택하지 않은 GC 방법들

방법 1) 세대별 GC 대신 CMS

세대별 GC라는 것이 있음. 세대별 가설 (새로 생성된 객체가 오래된 객체에 비해 일찍 죽는다는 가설. YB가 OB보다 빨리 죽는다.)에 근거한 알고리즘임. 예를 들어 임시 변수나 로컬 변수 같은 애들이 생명 주기가 짧은 것처럼.. 그래서 힙을 YG와 OG로 구분하고 YG에서 더 자주 GC를 수행함(Minor GC). 그러다가 YG에서 여러번 살아남은 객체를 OG로 보내고 OG는 덜 GC를 수행함(Major GC). 그런데 만약 YG의 객체를 OG의 객체가 참조하고 있었을 시 YG에서 GC가 그 객체를 메모리에서 해제해버리면 OG의 객체가 걔를 못 찾는 Dangling 이 발생할 수 있음. 그래서 Write barrier를 사용하여 세대 간 참조 사실을 기록했다가 검사하는 방식으로 댕글링을 방지함. 그런데 Go는 Write barrier에 대한 오버헤드가 싫어서 세대별 GC를 선택하지 않음. Go는 컴파일 시점에 escape 분석(탈출검사)로 메모리를 힙에 할당할지 스택에 할당할지 결정함. 그래서 객체를 좀더 가벼운 곳인 스택에 할당한다면 세대별 GC로 얻을 수 있는 성능 개선 효과가 좀 덜함. 세대별 GC는 힙에서 효과 있는 거니까.

방법 2) 압축방식 대신 비압축방식

또한 Go의 GC는 Compaction(압축 방식, 동적 유형)이 아닌 Non-compaction(비압축 방식, 정적 유형)을 택해서 메모리 해제 후 빈 공간을 그냥 냅둠.(압축 방식의 GC들은 중간중간 빈 곳을 쫙 압축해서 재배치함.) 메모리 할당과 GC를 반복하면 힙 메모리가 산산조각 나고(단편화) 할당도 느려짐. 성능이 악화됨.

Go는 비압축방식을 쓰지만 또 다른 tcmalloc(Thread caching memory allocation)이라는 방식으로 메모리 단편화와 할당 속도 문제를 해결했다고 함. tcmalloc은 힙을 ‘중앙힙’과 ‘Thread local cache(TLC)’ 영역으로 구분함. 메모리 할당시 불필요한 동기화가 줄어들어 lock 비용이 감소한다고 함.

 

'Go lang' 카테고리의 다른 글

Go 프레임워크 + 테스트코드 라이브러리  (0) 2024.12.20
Go 고루틴과 채널  (1) 2024.12.20
Go는 exception이 없다  (0) 2024.12.20
Go 컴파일 언어  (0) 2024.12.20
Go 구조체와 인터페이스  (1) 2024.12.20