Garbage Collection

Unity의 가비지 컬렉터

Unity의 가비지 컬렉터는 Boehm-Demers-Weiser 가비지 컬렉터를 사용한다. 이 가비지 컬렉터는 Mark and Sweep 알고리즘을 사용한다.

Mark and Sweep

  1. 루트 셋(전역 변수나 현재 함수의 로컬 변수)에서 참조를 따라간다.
  2. 방문한 객체에 마킹한다.
  3. 마킹 작업이 완료되면 마킹되지 않은 객체를 해제한다.

점진적 Garbage Collector

Unity의 가비지 컬렉터는 Stop-the-world다. 때문에 프로그램을 일시정지할 필요가 있어 프레임이 급감하는데, 이것이 GC 스파이크다. Project Setting에서 점진적 Garbage Collector를 활성화하면 이 작업을 여러 프레임에 나눠서 처리한다.
여러 프레임에 나눠서 처리하기 때문에 도중에 레퍼런스가 바뀌면 다시 스캔해야할 필요성이 생긴다. 이 과정에서 오버헤드가 발생한다.

Unity 가비지 컬렉터의 문제점

Unity의 가비지 컬렉터는 다른 가비지 컬렉터, 주로 .Net의 가비지 컬렉터와 비교해 여러 단점을 가지고 있다.

  1. 세대 구분이 없어 빈번한 임시 할당을 효율적으로 제거할 수 없다.
  2. 압축 과정이 없어 외부 메모리 단편화를 해결할 수 없다.

2번 같은 경우 용량이 큰 객체를 메모리에 할당할 때, 충분한 공간이 없으면 가비지 컬렉터를 실행한 뒤, 여전히 공간이 없다면 힙을 확장해버린다.

가비지 최적화

가비지 컬렉션은 보통 비용이 많이 드는 작업이지만, Unity는 그 영향이 더 심하다. 따라서 아예 가비지를 만들지 않는 것이 최선이 된다.

  1. 오브젝트 풀 사용 : 객체를 파괴하지 않고 메모리에 유지시킨다. 활성화/비활성화를 통해 생성/삭제와 비슷한 효과를 낼 수 있다.
  2. String Builder 사용 : string은 기본적으로 읽기 전용이다. + 같은 연산자를 통해 문자를 추가하는 경우, 문자가 추가된 string을 새로 생성하기 때문에 이전에 할당했던 string은 가비지 컬렉션의 대상이 된다.
  3. 클로저 최소화 : 클로저를 간단하게 설명하면, 무명 함수나 람다식에서 스코프 외부의 변수나 함수를 참조하는 것을 의미한다. 이 외부 변수를 올바르게 전달하기 위해 익명 클래스를 생성하게되고, 보통 무명함수나 람다는 일회용 함수로 사용하기 때문에 외부변수를 캡쳐하기 위해 생성했던 객체의 레퍼런스를 바로 잃어버린다. 이것이 가비지 컬렉션의 대상이 된다.
  4. 박싱 최소화 : 주로 자동으로 값 타입에서 참조 타입으로 변환될 때 발생한다.
  5. 코루틴 : WaitFor계열 객체를 yield 연산자에서 생성하기보다 캐싱해서 사용하는 것이 바람직하다.
  6. Linq와 정규표현식 지양 : 박싱으로 인한 가비지가 발생한다.
  7. Unity API : 몇몇 Unity API는 가비지를 생성한다. NonAlloc이 붙은 함수를 사용하는 것을 권장한다. 특히 배열 기반일 경우 접근할 때 마다 사본을 생성하는 경우가 있어, 가능하면 캐싱해서 사용하는 것이 좋다.
  8. 빈 배열 반환 : 길이가 0인 배열을 반환할 때 새로 생성하기보다, 미리 정의된 정적 인스턴스로 반환하는 편이 효율적이다.