[TIL] C#과 Unity의 GC(Garbage Collection)

조재훈·2024년 8월 21일

개요

면접 대비용 공부를 하다가 C#과 Unity의 GC는 비슷해보여도 차이점이 있다는 것을 알게 되었다. 개발을 하고 사람들과 대화하면서 GC에 대해 이야기 할 때도 있는데 GC가 뭐냐고 물으면 진짜 대충 안쓰는 객체 메모리 해제한다라고만 설명 못할 것 같아서 아예 GC를 블로그에 정리해보려고 한다

GC(Garbage Collection)

최근 C#, Java 이전의 C나 C++ 언어에서는 사용자가 직접 메모리를 할당하고 해제를 했어야 했다.(malloc, free / new, delete) 메모리를 직접 컨트롤하는 건 로우 레벨을 다루는 것이니 잘만 다루면 엄청난 성능을 뽑아낼 수 있을 것이다. 하지만 번거롭고 위험한 작업인 걸 많은 사람들이 공감할 것임

그래서 개발에만 집중할 수 있고 잦은 메모리 이슈에서 벗어나기 위해 GC가 등장했다. GC는 힙에 있는 더 이상 사용되지 않는 메모리 영역을 알아서 해제해 가용 메모리로 돌려주는 것인데 실행 시점과 메모리 해제 시점을 정확히 예측하기 어려워 메모리 사용량이나 성능 최적화가 복잡해질 수 있음. 직접 GC를 호출할 수 있지만 시스템의 성능에 큰 영향을 주기에 직접 호출은 피하자

그리고 메모리가 부족해 GC가 동작할 때에는 다른 동작들을 다 멈추기 때문에 오버헤드가 발생할 수 있고 이를 전문적으로 Stop-The-World라 한다

이로 인해 GC가 너무 자주 호출되면 소프트웨어의 성능이 하락해 좋은 소프트웨어가 되지 못할 것임. 특히 게임과 같은 실시간 성능이 중요한 소프트웨어에서는 예측 불가능한 GC는 오히려 독이 될 수 있다

하지만 독도 잘 쓰면 약이 될 수 있기 때문에 개발자들이 효율적으로 GC가 동작하게 코드를 작성하는 최적화 작업에 힘을 쓸 수 밖에 없다

C#의 GC 동작


출처

GC는 프로그램 내 루트 객체에서 시작해, 어떤 객체가 참조되고 있는 지를 탐색한다. 더 이상 어디에서도 참조되고 있지 않는 객체가 있을 경우 그 메모리를 해제한다

Mark And Sweep

GC에서 주로 사용하는 알고리즘이다. 간단히 말해 사용중인 객체를 Mark하고 Mark 되지 않은 객체를 Sweep하여 메모리에서 제거하는 방식임

Compact 과정 : Sweep 후 분산된 메모리들을 Heap의 시작 주소로 모아 메모리가 할당된 부분과 그렇지 않은 부분으로 압축한다(GC의 종류에 따라 하지 않을 수도 있음)

세대

C#에서 GC는 총 3세대의 세대를 나눠 관리한다. GC가 필요한 순간 실질적으로 오래 사용되는 객체가 있고 잠깐 사용하는 객체가 있는데 오래 사용하고 있는 객체의 경우 계속해서 사용 중인지 탐색하는 것은 부담스러울 수 있다

그래서 객체가 얼마나 오래 사용되고 있느냐를 기준으로 해당 객체를 탐색하는 빈도를 줄이기 위해 세대라는 개념을 도입했다

세대는 몇 번의 GC를 거쳤는 지를 나타내는데 GC가 일어나고 스택 메모리에서 참조되고 있는 오브젝트들에 대해서는 세대 수를 증가시킨다. 그리고 GC는 기본적으로 낮은 세대에서부터 메모리를 탐색하게 된다

Unity의 GC

Unity의 GC는 일반적으로 C#의 GC와 비슷하나 Boehm-Demers-Weiser 라는 알고리즘을 통해 GC 작업을 하게 된다. Mark And Sweep과 비슷하지만 세대 구분이 없고 메모리 정렬이 없어 C#의 GC보다는 더 최적화에 신경을 써야 한다

GC 최적화

GC가 호출되면 순간적으로 오버헤드가 발생할 수 있기에 개발자는 최대한 메모리 관리를 잘해서 GC가 호출되는 일이 별로 없게 최적화를 해야 한다

주로 개발자가 할 수 있는 최적화에는

  • 쓸모 없는 객체 생성 줄이기(메모리 할당 최소화)
  • 객체 생성을 많이 해야할 때에는 Object Pooling 사용
  • 리스트나 배열의 크기를 미리 할당
  • String 조작이 많을 경우 StringBuilder 사용
  • foreach 대신 for 사용
    • foreach 문은 컬렉션을 순회할 때 임시 변수를 할당하므로 큰 컬렉션을 순회할 때는 for을 사용하는 것이 좋음
  • 작은 크기의 데이터는 클래스 대신 구조체 사용
  • 이벤트나 델리게이트 사용 주의
    • 이벤트 구독 관리 : 구독/해지 신중히 결정 후 사용하지 않는 이벤트는 빨리 해지
    • 람다 표현식 주의 : GC의 대상인 클로저를 생성하므로, 자주 사용되는 곳에서는 피하기
  • 함수 내에서 자주 생성되는 객체를 미리 할당 : 클래스 필드로 객체를 할당해 놓고 재사용
profile
나태지옥

0개의 댓글