오늘은 언리얼 엔진의 가비지 컬렉션에 대해 공부하겠습니다.
1. 가비지 컬렉션?
가비지 컬렉션이란 메모리를 관리해주는 시스템으로 C#, JAVA 등 언어에는 기본적으로 가지고 있는 시스템입니다.
힙 메모리에 영역을 할당받으면 자동으로 가비지 컬렉션 시스템에 감시를 받으며 만약 더 이상 사용되지 않는 영역을 자동으로 해제작업을 해주는 시스템입니다.
하지만 이런 시스템은 C++에는 없으므로 수동으로 해제해주거나 스마트 포인터에 힘을 빌려서 사용해야합니다.
그렇치만 언리얼에서는 이 가비지 컬렉션을 지원을 해줍니다.
2. 언리얼의 가비지 컬렉션
언리얼에서 UPROPERTY 리플렉션을 붙인 객체는 언리얼 엔진이 자동으로 가비지 컬렉터를 이용하여 메모리를 관리합니다.
가비지 컬렉션을 수행함에 있어 리플렉션 시스템을 사용하는데, 엔진이 객체와 속성값을 알고 있으므로, 더 이상 사용되지 않아 삭제해도 괜찮은 객체들을 구분할 수 있기 떄문입니다.
언리얼 엔진에서는 Reference Graph를 만들어 오브젝트들의 사용 여부를 구분합니다. 이 그래프 루트에는 "Root Set" 이라 지정된 오브젝트 셋이 존재하며 "Root Set" 에 포함된 객체들은 가비지 컬렉션 대상에서 제외됩니다.
UObject::BaseUtility::AddToRoot 함수를 이용하면 객체를 "Root Set" 에 추가시킬 수 있다.
가비지 컬렉션이 실행되면 엔진은 "Root Set" 을 시작으로 UObject 레퍼런스 트리를 검색해 참조된 오브젝트를 모두 추적한다. 이 검색 과정에서 찾지 못한 것들은 더 이상 필요하지 않는 오브젝트라고 판단하고 제거할 수 있는 것 입니다.
이는 가비지 컬렉션이 리플렉션 데이터에 의존하므로 가능한 일 입니다.
3. 규칙
이 가비지 컬렉션을 사용할려면 몇가지 규칙이 있습니다.
1. UPROPERTY 선언
클래스 내부 맴버 변수가 클래스의 객체의 수명과 운명을 함께한다면 UPROPERTY로 선언해야 한다. 이는 수명 주기가 같다는 뜻이며 반면 잠시 사용할 UObject나 일반 클래스 객체들은 별도로 관리해 주어야 합니다.
2. 맴버가 가리키는 포인터
엔진이 인식하거나 관리하지 않는 메모리 영역을 가리키도록 만들면 안된다. 따라서 맴버가 가리키는 포인터는 UObject또는 그 자식들로 한정시키는 것이 좋다.
3. TArray를 활용하자
UObject 또는 자식들에 대한 포인터를 안전하게 담을 수 있는 컨테이너는 TArray가 유일하다.
예시 코드
UCLASS()
class AMyActor : public AActor
{
GENERATED_BODY()
public:
UPROPERTY()
MyGCType* SafeObject;
MyGCType* DoomedObject;
AMyActor(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
SafeObject = NewObject<MyGCType>();
DoomedObject = NewObject<MyGCType>();
}
};
void SpawnMyActor(UWorld* World, FVector Location, FRotator Rotation)
{
World->SpawnActor<AMyActor>(Location, Rotation);
}
위에서 SafeObject는 UPROPERTY()가 선언이 되어 있으므로, Root Set Object로부터 참조가 가능해 가비지 컬렉션 대상이 되지 않는다. 하지만 DoomedObject는 Root Set Object로부터 참조가 되지 않아 가비지 컬렉팅이 되고 결국 댕글링 포인터로 남아 있을 수 있다.
특정 UObject가 가비지 컬렉션되면, 모든 UPROPERTY Reference가 Null 로 세팅된다. 이는 오브젝트가 가비지 컬렉션 되었는지 아닌지를 안전하게 검증할 수 있게 만들어 준다.
if (MyActor->SafeObject != nullptr)
{
// Use SafeObject
}
위에서 MyActor->SafeObject가 nullptr가 아니라는 뜻은 해당 UObject가 가비지 컬렉팅을 기다리지 않다는 뜻이다. 엑터를 Destroy하게 되면, 가비지 컬렉터가 다시 실행되기 전까지는 실제로 제거되지 않으므로, IsPendingKill 메소드를 사용하여 해당 UObject가 수거를 기다리고 있는지 체크 할 수 있다.
4. UStruct UObject
구조체는 Value 타입으로 사용하기 위한 것이다. 구조체는 가비지 컬렉션의 대상이 아니므로, UObject내에 위치해야 메모리가 올바르게 회수될 수 있다.
UStruct의 장점은 크기가 매우 작다는 것인데, UObject는 데이터 외에도 book-keeping 데이터를 거쳐야 하지만, UStruct는 사용자가 입력한 크기 만큼만 사용하기 떄문이다 반변, UStruct는 다른 객체의 맴버 구조체를 직접 가리키는 것이 안전하지 않다는 단점 또한 존재한다.
UObject는 UStruct에 비해 무겁지만 안전하게 사용할수 있다.
5. 명시적 요청
UObject들은 가비지 컬렉션을 명시적으로 요청할 수 있다. 해당 함수들을 호출하면, 가비지 컬렉션 수행 대상으로 등록되게 된다.
1) UObject
UObject::ConditionalBeginDestroy()
2) AActor
AActor::DestroyActor()
6. Ref
'개발자 면접 공부 > 언리얼 엔진' 카테고리의 다른 글
RTTI vs Reflection (2) | 2024.03.11 |
---|---|
FSM, HFSM, BT 의 구조 (0) | 2023.02.28 |
언리얼 엔진의 리플렉션 (프로퍼티) (0) | 2023.02.02 |
언리얼 엔진의 CDO 로딩 과정 등 (0) | 2023.01.31 |
#9 에디터내 테스팅(플레이 & 시뮬레이트) (0) | 2020.11.11 |