[Unity] 유니티 메모리 관리와 가비지 컬렉터(Garbage Collector)
개발을 할 때 메모리 관리는 필수 요소이다.
메모리를 관리하지 않는다면 메모리 누수로 인해 예기치 못한 상황이 발생하게 될 것이다.
Unity에서는 C# .NET Framework의 기반인 만큼 가비지 컬렉터가 존재한다.(C# 가비지 컬렉터)
하지만 Unity의 가비지 컬렉터는 .NET Framework에서 지원하는 가비지 컬렉터와는 조금 다른데,
그 부분과 유니티의 Managed Heap은 어떤 식으로 관리되는지 알아보려고 한다.
유니티 가비지 컬렉터 작동 방식
유니티는 총 두가지의 방식으로 가비지 메모리를 수집할 수 있다.
보엠 가비지 컬렉션(Boehm Garbage Collection)
기본적으로 유니티 가비지 컬렉터는 Boehm GC를 사용하고 있다.
Boehm GC는 Mark and Sweep 알고리즘을 사용한다.
Boehm GC는 Stop-the-World 가비지 콜렉터이므로 유니티 가비지 컬렉션이 시작될 땐 프로그램 전체가 멈춘다.
즉 정리해야할 메모리가 많아진다면 그만큼 대기해야하는 시간이 길어지므로 가비지 컬렉터 호출을 최소화 해야한다.
점진적 가비지 컬렉션(Incremental Garbage Collection)
유니티는 2019.1에 점진적 가비지 컬렉션을 추가했다.
기존 Boehm GC같은 경우 가비지 컬렉션 수행 되는 동안 프로그램이 멈추게 되지만,
점진적 가비지 컬렉션을 사용하게 될 경우 작업을 다수 프레임으로 분산시켜 프로그램 실행을 보다 더 짧게 중단시킨다.
점진적 가비지 컬렉션을 활성화 하는 방법은 다음과 같다.( Edit > ProjectSettings > Player > Other Settings > Configuration > Use Incremental GC )
기본적으로 Mark and Sweep 방식을 통해 메모리를 관리하지만 Compaction 과정을 거치지 않기 때문에 메모리 누수 현상은 피해갈 수 없는 문제이다.
Unity Managed Heap
.NET Framework의 가비지 컬렉터는 세대별 가비지 컬렉션( Generation Garbage Collection)으로,
세대별로 관리되는 힙을 나누어 메모리를 관리한다.
반면 Unity의 힙은 세대별, SOH와 LOH 등으로 힙을 따로 나누지 않고 모든 객체들을 한 곳에서 관리한다.
또한 Compaction 과정을 하지 않으므로 위의 그림과 같이 메모리 세그먼트 사이에 "간극"이 생기게 되는데,
이 간극에는 해당 공간의 용량보다 작거나 같은 데이터밖에 저장할 수 없게 되어 메모리 누수가 발생하게 된다.
위와 같이 int형 배열을 메모리에 할당하려고 할 때 전체적으로 할당 가능한 충분한 메모리임에도 간극으로 인해 메모리를 할당하지 못할경우 유니티 메모리 관리자는 어떻게 할까?
유니티 메모리 관리자는 위와 같은 상황에 처했을 때 다음과 같은 작업을 실행한다.
- 우선, 가비지 컬렉터가 아직 실행되지 않은 경우 이를 실행시키고, 수용 가능한 충분한 공간을 만들 수 있도록 시도한다.
- 가비지 컬렉터가 실행된 이후에도 수용가능한 충분한 공간이 없는 경우에는 힙을 확장시킨다. 이때 힙이 확장되는 구체적인 양은 플랫폼에 따라 다르지만, 대부분의 플랫폼에서는 힙이 확장될 때 이전 확장의 두 배만큼 확장된다.
이렇게 늘어난 힙 공간은 더 큰 할당이 발생하는 경우 힙을 재확장해야 할 필요가 없도록 하기 위해 많은 빈 공간이 존재하여도 그대로 유지한다.
하지만 이러한 방식은 결국 메모리를 무지하게 잡아먹는 원인이 되고,더 이상 확장할 메모리가 없게 된다면 예기치 못한 상황이 발생하게 될 것이다.
마무리
무분별한 메모리 할당은 잦은 가비지 컬렉터 호출로 이어지며,
더 큰 힙 메모리로 확장하게 되면서 시스템의 많은 메모리를 차지하게 될 것이다.
이러한 문제는 곧 성능 저하로 이어지며 사용자에게 좋지 않은 경험을 제공하기 때문에 더더욱 최적화에 신경을 써야한다.
예를 들어 재사용성이 높은 객체의 경우 한 번 사용한 이후 Destory 메서드를 호출하여 객체를 파괴하는 방식보단,
ObjectPooling을 이용하여 객체를 미리 할당한 이후 OnEnable과 OnDisable을 통해 객체를 재사용 하는 방식으로 메모리 누수를 방지하거나,
문자열 연산을 반복할 경우 StringBuilder 클래스를 통해 객체를 생성하지 않고 객체를 변형하는 방식을 사용하는 것이 좋다.
또한 유니티 프로파일러를 통해 메모리를 실시간 확인하며 관리하는 것도 좋은 방법이다.
참고 문서 : https://docs.unity3d.com/kr/2021.2/Manual/performance-managed-memory.html