일반적으로 Unity Engine에서는 C#을 사용하기 때문에 메모리를 알아서 관리해주는 GC (가비지컬렉션)을 사용한다. 하지만 일반적인 .Net 환경이 아니라 Mono 환경이기 때문에 실제 MS에서 제공하는 .Net의 GC와는 다른 알고리즘을 채택해서 사용한다. 엔진에서 쓰고 있는 GC 알고리즘은 Boehm GC 이다 (정확히는 Boehm-Demers-Weiser GC)이는 .Net에서 쓰고있는 GC 알고리즘 (Mark and Sweep)은 세대별구분을 하여 메모리 단편화에 대한 정렬까지 지원해주는 방식이지만, Unity Engine에서는 메모리 단편화에 대한 정렬을 해주지 않는 방식을 사용한다는 것이다.
Managed Heap
객체, 문자열, 배열 등이 생성될 때 그 데이터를 저장하기 위한 공간은 메모리 구조에서 Heap 메모리에 해당한다. 엔진의 Mono, 혹은 IL2CPP에서의 메모리 관리자가 자동으로 관리해주는 Heap 공간을 Managed Heap이라고 말한다. 코드에서 생성되는 참조타입, 박싱된 값 타입은 모두 이 공간에 할당된다.
값타입 , 참조 타입
함수 호출시 파라미터의 값은 복사되어 전달된다. 기본 자료형들은 크기가 작아서 빠르게 쉽게 복사 될 수 있지만, 오브젝트, 스트링, 배열 등은 훨씬 큰 경우가 많아서 계속해서 복사 하기에는 매우 비효율적이다.
그래서 이런 큰 원소들을 Heap 공간에 저장하고 그 메모리 공간을 나타내는 포인터 주소값으로 복사해서 전달 한다.
인자 전달시 값이 복사되는 타입을 “값타입”이라고 하고 (C#에서는 구조체도 값타입이고, 엔진에서 제공하는 Color, Vector3등이 대표적인 구조체라고 할 수 있다.)
Heap 공간에 저장해서 포인터로 주소에 접근하는 타입을 “참조 타입”이라고 한다. (여기에는 C++과 마찬가지로 문자열, 배열은 포함이고, 클래스를 실체화한 객체들은 무조건 참조타입이다.)
엔진에서 채택하고 있는 Boehm 방식에 대한 설명
Boehm GC
엔진에서 채택하고 있는 방식은 위에 설명한 .Net의 GC와는 다르게 비세대, 비압축(메모리 단편화가 존재하는 의미) 방식으로 동작 한다.
Non-Generational (비세대)
GC가 컬렉션을 수행할 때 Heap에 할당된 모든 오브젝트들을 순회한다는 의미이다. 따라서 Heap에 할당된 오브젝트의 수가 많아질 수록 성능이 저하 된다.
Non-Compaction (비압축)
더 이상 참조되지 않는 오브젝트가 해제된 이후 오브젝트들 사이의 빈 공간을 위해서 재배치(메모리 정렬)을 하지 않는다는 것을 뜻한다. 이는 메모리 파편화를 유발한다 (외부 단편화, 메모리공간이 조각 난다.)
위 그림이 메모리 파편화의 예시이다.
만약에 파편화가 되어있으면 남은 전체의 공간이 충분하더라도 공간이 연속적이지 못해서 메모리 할당을 할 수 없게 된다.
GC 이후에도 공간이 생기지 않으면 엔진은 Managed Heap의 공간을 2배로 늘리게 된다.
이렇게 한번 늘어나면 크기를 잘 줄이지 않는다. 언젠가 다시 더 큰 메모리가 필요할 수 있으니까. 그리고 언제가 될지 모른다.
Stop-the-world
엔진에서 채택한 Boehm GC는 Stop-the-world 방식이다. 즉, GC 스레드가 시작되면 프로그램을 멈추고 GC 스레드가 끝나기까지 기다리기 때문에 프리징현상이 생긴다. Heap에 할당된 오브젝트의 개수가 많다면 그 만큼 길어진다. 이 현상은 GC Spike라고도 한다.
따라서 엔진을 사용하여 게임을 개발 할 때 최대한 GC가 작동하지 않도록 최적화 하는 것이 중요하다.