면접 대비용 공부를 하다가 C#과 Unity의 GC는 비슷해보여도 차이점이 있다는 것을 알게 되었다. 개발을 하고 사람들과 대화하면서 GC에 대해 이야기 할 때도 있는데 GC가 뭐냐고 물으면 진짜 대충 안쓰는 객체 메모리 해제한다라고만 설명 못할 것 같아서 아예 GC를 블로그에 정리해보려고 한다
최근 C#, Java 이전의 C나 C++ 언어에서는 사용자가 직접 메모리를 할당하고 해제를 했어야 했다.(malloc, free / new, delete) 메모리를 직접 컨트롤하는 건 로우 레벨을 다루는 것이니 잘만 다루면 엄청난 성능을 뽑아낼 수 있을 것이다. 하지만 번거롭고 위험한 작업인 걸 많은 사람들이 공감할 것임
그래서 개발에만 집중할 수 있고 잦은 메모리 이슈에서 벗어나기 위해 GC가 등장했다. GC는 힙에 있는 더 이상 사용되지 않는 메모리 영역을 알아서 해제해 가용 메모리로 돌려주는 것인데 실행 시점과 메모리 해제 시점을 정확히 예측하기 어려워 메모리 사용량이나 성능 최적화가 복잡해질 수 있음. 직접 GC를 호출할 수 있지만 시스템의 성능에 큰 영향을 주기에 직접 호출은 피하자
그리고 메모리가 부족해 GC가 동작할 때에는 다른 동작들을 다 멈추기 때문에 오버헤드가 발생할 수 있고 이를 전문적으로 Stop-The-World라 한다
이로 인해 GC가 너무 자주 호출되면 소프트웨어의 성능이 하락해 좋은 소프트웨어가 되지 못할 것임. 특히 게임과 같은 실시간 성능이 중요한 소프트웨어에서는 예측 불가능한 GC는 오히려 독이 될 수 있다
하지만 독도 잘 쓰면 약이 될 수 있기 때문에 개발자들이 효율적으로 GC가 동작하게 코드를 작성하는 최적화 작업에 힘을 쓸 수 밖에 없다
GC는 프로그램 내 루트 객체에서 시작해, 어떤 객체가 참조되고 있는 지를 탐색한다. 더 이상 어디에서도 참조되고 있지 않는 객체가 있을 경우 그 메모리를 해제한다
GC에서 주로 사용하는 알고리즘이다. 간단히 말해 사용중인 객체를 Mark하고 Mark 되지 않은 객체를 Sweep하여 메모리에서 제거하는 방식임
Compact 과정 : Sweep 후 분산된 메모리들을 Heap의 시작 주소로 모아 메모리가 할당된 부분과 그렇지 않은 부분으로 압축한다(GC의 종류에 따라 하지 않을 수도 있음)
C#에서 GC는 총 3세대의 세대를 나눠 관리한다. GC가 필요한 순간 실질적으로 오래 사용되는 객체가 있고 잠깐 사용하는 객체가 있는데 오래 사용하고 있는 객체의 경우 계속해서 사용 중인지 탐색하는 것은 부담스러울 수 있다
그래서 객체가 얼마나 오래 사용되고 있느냐를 기준으로 해당 객체를 탐색하는 빈도를 줄이기 위해 세대라는 개념을 도입했다
세대는 몇 번의 GC를 거쳤는 지를 나타내는데 GC가 일어나고 스택 메모리에서 참조되고 있는 오브젝트들에 대해서는 세대 수를 증가시킨다. 그리고 GC는 기본적으로 낮은 세대에서부터 메모리를 탐색하게 된다
Unity의 GC는 일반적으로 C#의 GC와 비슷하나 Boehm-Demers-Weiser 라는 알고리즘을 통해 GC 작업을 하게 된다. Mark And Sweep과 비슷하지만 세대 구분이 없고 메모리 정렬이 없어 C#의 GC보다는 더 최적화에 신경을 써야 한다
GC가 호출되면 순간적으로 오버헤드가 발생할 수 있기에 개발자는 최대한 메모리 관리를 잘해서 GC가 호출되는 일이 별로 없게 최적화를 해야 한다
주로 개발자가 할 수 있는 최적화에는
Object Pooling 사용