들어가기에 앞서 용어 정리가 필요하다. 'GC' 된다고 하는 것은 필요 없는 UObject 가 정리 된다는 것을 뜻한다. 기본적으로 UPROPERTY(...) 로 제대로 Referencing 하고 있으면 'GC' 가 되지 않는다. 가끔 UPROPERTY 에 있어야 GC대상이 된다고 하는 분들이 있는데 엄밀히 말해서는 반대 이야기다. UPROPERTY 등으로 Referencing 이 되어 있지 않으면 해당 UObject 는 GC당하는데 Pointer 변수값이 nullptr 로 변경되지 않아서 다른 문제가 생긴다.
이 포스팅에서 다음에 초점을 두어 본다.
UE GC를 설명할때 매우 중요한게 TWeakObjectPtr<> 인데 이 건 다른 포스팅에서 따로 다룬다.
이 내용은 공식문서나 인터넷 상의 여러 Article 에 잘 설명되어 있으므로 여기서는 간략히 살펴만 보고 넘어가자.
UnrealEngine 은 Mark and Sweep 방식으로 Garbage Collection(이하 GC) 를 수행한다. Root 로 명시된 Object 로 부터 Reflection 기능을 기반으로 Object Refrence Property 및 추가 함수 기능을 이용해서 Rechable Object 를 찾는다. 실제로 수행은 반대인데 처음에 Global Object Array 에서 Object Flag 에 Unreachable 을 한번에 모조리 마킹해두고, Referencing 될떄마다 Unreachable 을 지운 다음, Sweep 에서 Unreachable 이 살아있는 Object 를 제거한다.
생각해 보면 알 수 있듯이 Mark 동작을 먼저하고, Sweep 동작은 이후에 진행해도 로직 상으로는 문제가 없다. 다만 최적화를 구현함에 있어 세부적인 방식은 Multi-Thread 까지 동원해서 수시로 변경되고 있다(볼때마다 달라져 있더라... 는 개인적인 후문이).
이상한 코딩만 하지 않으면 한번 Unrechable 로 판명된 Object가 다시 Reachable 이 될 수 는 없다.
Marking 방식 및 GC되지 않게 하는 방식은 정말 많은데 짚고 넘어갈 것은 Native(C++ Code)에 있는 Class 는 GC대상이 아니라는 것이다. 당연한거 아냐? 라고 생각하셨으면 'UClass Reflection' 포스팅을 잘 안 보신거다.
UCLASS 도 UObject 이기 떄문에 기본적으로 관리 대상이다. UClass, UClass-CDO 및 그기서 Referencing 되고 있는 Object도 당연히 관리 대상이다.
그럼 Blueprint 로 만들어진 UClass 는? Referencing 하지 않으면 당연히 날아간다(GC된다). Blueprint 로 만든 Object 를 Referencing 만 해도 해당 UCLASS 는 참조 되므로 잘 인식하지 못하고 사용할 수 있는데 날아 갈 수 있다는걸 항상 주의해야 한다.
오직 모든 UObject Instance 만 Garbage Collection 관리 대상이다. '모든 UObject' 을 기억해 두자.
UnrealEngine 은 C++ 로 Garbage Collection(이하 GC) 기능을 직접 만들어 두었다. C# 같은 Managed Memory 언어가 아닌 C++ 을 쓰면서 GC 를 직접 제공하면 어떤 이점이 있을까? 가장 큰 장점은 프로그래머가 원하는 대상에 한해서 GC 관리를 받을 수 있다는 점이다.
프로그래머가 원하는 대상에 한해서 GC 관리를 받을 수 있다는 것은, GC 수행 시간을 줄일 수 있고, 필요한 부가 Memory 또한 줄일 수 있다는 뜻이 된다. Managed Memory 기반 GC 언어들은 모든 Object 가 GC 대상이기 때문에 상당한 부하가 따른다.
stop-the-world 는 GC가 수행될때 All-Stop 시키고 수행함으로서 모든 연산이 순간 멈춘다는 뜻으로 GC 방식의 가장 큰 단점이다. UE 는 직접 구현함으로서 그 대상을 줄이고 최적화할 수 있다. 그래도 과도하게 UObject 를 만들면 멈칫 하긴 한다.
Referencing 되고 있음에도 임의로 GC 대상이 되게 할 수 있다. UE 에서 대표적인 방식이 AActor::Destroy(...) 인데 예전 Code 에서는 MarkPendingKill 계열 Flag를 사용 했으나 근래에 UObjecT::MarkAsGarbage() 를 통한 RF_MirroredGarbage 로 변경 된듯 하다. 여기서는 Referencing 되고 있어도 GC 될 수 있게 할 수 있다는 점만 명심하자.
Unity 경험자의 경우 Unity 도 임의로 destroy 할 수 있고 destroy 이후에 if (obj == null) 로 확인하면 true 라고 생각하는 분이 계실지도 모르겠다.
Unity 의 내부 C# 은 Mono 등에서 여러가지로 변경되었고, GC 관리 방식도 공식 .net framework 과는 다르다. 하지만 기본 개념은 같다. 그 말은 임의로 Object 를 파괴할 수 없다는 뜻이다(dispose 로 일부 내용을 해제하는 것과는 다르게 Object 완전 파괴의 경우).
그럼 위에서 null 로 확인은? Destroy(gameObject)를 하게 되면 해당 gameObject는 World 에서 빠지게 되고 operator== 재정의로 World 소속 여부를 물어서 true/false 판정을 하게 된다. 'if(obj == null)' 이 참이라고 해서 다른 gameObject 에서 변수로 참조하고 있으면 해당 gameObject 의 C#-Object 는 파괴되지 않는다(혹시 custom c#으로 인해 이부분에 변경이 있었으면 꼭 댓글 부탁 드립니다).
UE에서 Object 유효성을 검사하는 가장 중요한 함수가 IsValid 인데, 내부 구현이 상당히 자주 변경되어 왔다. 하지만, 기본적인 작동 방식은 크게 변하지 않았다.
/**
* Test validity of object
*
* @param Test The object to test
* @return Return true if the object is usable: non-null and not pending kill or garbage
*/
FORCEINLINE bool IsValid(const UObject *Test)
{
return Test && FInternalUObjectBaseUtilityIsValidFlagsChecker::CheckObjectValidBasedOnItsFlags(Test);
}
struct FInternalUObjectBaseUtilityIsValidFlagsChecker
{
FORCEINLINE static bool CheckObjectValidBasedOnItsFlags(const UObject* Test)
{ // Here we don't really check if the flags match but if the end result is the same
checkSlow(GUObjectArray.IndexToObject(Test->InternalIndex)->HasAnyFlags(EInternalObjectFlags::Garbage) == Test->HasAnyFlags(RF_MirroredGarbage));
return !Test->HasAnyFlags(RF_MirroredGarbage);
}
};
여기서는 IsValid 가 하는게 nullptr 인지, Garbage 대상으로 체크되어 있는지 두개를 확인한다는걸 명심하자.
Test 대상이 되는 UObject* Test 변수 값은 생성후 아래 3개의 상태를 가진다고 할 수 있다.
이게 TObjectPtr 이 UPROPERTY 로 제대로 지정 되어 있어야 하는 이유이다. 즉, UObject* 변수의 값이 가르키는 Object 가 GC가 되면 메모리는 해제(엄밀히 말해서 재사용 대상) 되므로 해당 변수 값이 반드시 null 로 바뀌어야 한다. 이 동작을 위해서라도 해당 변수는 Reflection 대상 및 수동으로 관리 대상이 되어야 한다.
아닐 경우 일어 날 수 있는 최악의 참사를 생각해 보자.
하지만 다행히 대부분의 경우 GC되고 난 다음 위 Target 변수를 사용하면 바로 Crash 가 난다. 그렇지만 최악의 경우는 항상 염두해 두자.
UObject 를 AddToRoot 등으로 Root 로 등록하면 GC Referencing 출발점이 되고 GC당하지 않는다. 그럼 Root 로 부터 출발 해서 Reachable 로 판명할 수 있게 하는 Referencing 방법들을 살펴보자.
대표적인 방법으로 Reflection 기능으로 참조가 들어간다.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MyActor.generated.h"
UCLASS()
class MYGAME_API AMyActor : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
AMyActor();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
// TObjectPtr를 UPROPERTY로 선언하여 GC가 추적 가능하게 설정
UPROPERTY()
TObjectPtr<class AOtherActor> OtherActorPtr;
// AOtherActor와 상호작용하는 함수
void InteractWithOtherActor();
};
IsValid 에서도 언급했는데 반.드.시. UPROPERTY() 로 선언되어 있어야 함을 잊지 말자. 특히나 AActor 의 경우 Referencing 하고 있어도 Destroy 로 임의로 GC될 수 있기 떄문에 꼭 UPROPERTY() 로 선언해야 한다(사실은 TWeakObjectPtr 이 대부분의 경우 더 적절하다).
그럼 USTRUCT는 어떻게 관리 될까? 아래 코드를 보자
#pragma once
#include "CoreMinimal.h"
#include "OtherActor.h" // AOtherActor 클래스 포함
#include "MyStruct.generated.h"
// USTRUCT 정의
USTRUCT(BlueprintType)
struct MYGAME_API FMyStruct
{
GENERATED_BODY()
// TObjectPtr 변수를 UPROPERTY로 선언
UPROPERTY()
TObjectPtr<AOtherActor> OtherActorPtr;
// 생성자
FMyStruct()
: OtherActorPtr(nullptr) // 초기값을 nullptr로 설정
{
}
};
언리얼에서 GC관리 대상은 UObject 이다. 따라서 USTRUCT 는 관리 대상이 아니다. 그럼 위 Code 안에 있는 OtherActorPtr 가 제대로 참조되게 할려면 어떻게 해야할까? 해당 USTRUCT 가 UPROPERTY 로 UObject 에서 참조 되어야 한다. USTRUCT 는 UObject 와 달리 기본적으로 '0' 으로 초기화 되지 않음도 주의하자.
정리하면 UObject 만 GC관리 대상이므로 바로 참조는 되지 않지만, USTRUCT 도 Reflection Code 를 만들어 내기 때문에, 자신이 참조받게 되면 자신이 참조하는 내용을 연결할 수 있다.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MyStruct.h" // FMyStruct 포함
#include "MyActor.generated.h"
UCLASS()
class MYGAME_API AMyActor : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
AMyActor();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
// FMyStruct을 UPROPERTY로 선언하여 GC 및 리플렉션에 사용 가능하게 함
UPROPERTY()
FMyStruct MyStructProperty;
// AOtherActor와 상호작용하는 함수
void InteractWithOtherActor();
};
UObject 에서 아예 출발하지 않고 Reflection 도 사용하지 않으면서 Referencing 할 수 있는 방법중 하나가 FGCObject 를 상속 받는 것이다.
FGCObject Code 를 보면 생성자에서 Global GGCObjectReferencer 에 등록하는 것을 볼 수 있다.
FGCObject
/**
* This class provides common registration for garbage collection for * non-UObject classes. It is an abstract base class requiring you to implement * the AddReferencedObjects() method. */
class FGCObject
{
public:
/**
* The static object referencer object that is shared across all * garbage collectible non-UObject objects. */ static COREUOBJECT_API UGCObjectReferencer* GGCObjectReferencer;
/** Initializes the global object referencer and adds it to the root set. */
static COREUOBJECT_API void StaticInit();
/**
* Tells the global object that forwards AddReferencedObjects calls on to objects * that a new object is requiring AddReferencedObjects call. */ FGCObject()
{
RegisterGCObject();
}
구현은 이렇게 하면 된다.
#pragma once
#include "CoreMinimal.h"
#include "UObject/GCObject.h"
struct FMyReference : public FGCObject
{
public:
// UObject 참조
UObject* MyReferencedObject;
FMyReference()
: MyReferencedObject(nullptr)
{
}
// FGCObject 인터페이스: GC 시스템에 참조된 객체를 등록
virtual void AddReferencedObjects(FReferenceCollector& Collector) override
{
if (MyReferencedObject)
{
// Unreal Engine의 가비지 컬렉터에게 참조된 UObject를 추가
Collector.AddReferencedObject(MyReferencedObject);
}
}
// 필요한 경우 구조체의 메모리 정리를 위한 함수
virtual FString GetReferencerName() const override
{
return TEXT("FMyReference");
}
};
여기서 중요한건 void AddReferencedObjects(FReferenceCollector& Collector) 에서 어떤 임의의 조건에 따라서 (예를 들어 짝수번 호출 떄마다) Collector.AddReferencedObject(MyReferencedObject); 하면 안된다는 점이다. 설마 그렇게 하는 분은 안계시겠지만 하면 무슨 일이 일어날까? Referencing Chain 이 끊어지는 거야 당연한거고, MyReferencedObject 변수 값이 GC 된 시점에 null 로 변경되지 못.한.다.
일단 주의하자. FGCObject::AddReferencedObject(...) 가 아니다. 제일 대표적인 사용처는 AActor 에서 찾아 볼 수 있다. Super::AddReferencedObject 를 통해 부모 클래스 까지 올라가는 것을 볼 수 있다. UObject 계열에서 Refelction 에 의존하지 않고 Referencing 할 수 있는 방법이다. 또 Collector 를 일반 함수로 던져서 Non-UObject 나 Non-FGCObject 계열에서도 Rerferencing 걸 수 있다.
ObjectMactros.h 에 있는 struct FUObjectCppClassStaticFunctions 로 등록되고 GarbageCollector 에서 수집할 때 해당 Object 의 Class 기반으로 호출 된다.
AActor
static ENGINE_API void AddReferencedObjects(UObject* InThis, FReferenceCollector& Collector)
void AActor::AddReferencedObjects(UObject* InThis, FReferenceCollector& Collector)
{
AActor* This = CastChecked<AActor>(InThis);
Collector.AddStableReferenceSet(&This->OwnedComponents);
#if WITH_EDITOR
if (This->CurrentTransactionAnnotation.IsValid())
{ This->CurrentTransactionAnnotation->AddReferencedObjects(Collector);
}#endif
Super::AddReferencedObjects(InThis, Collector);
}
World 상의 AActor 에 대한 Referencing 도 AddReferencedObjects 을 통해서 이루어 진다.
void ULevel::AddReferencedObjects(UObject* InThis, FReferenceCollector& Collector)
{
ULevel* This = CastChecked<ULevel>(InThis);
// Let GC know that we're referencing some AActor objects
if (FPlatformProperties::RequiresCookedData() && GActorClusteringEnabled && This->bGarbageCollectionClusteringEnabled && This->bActorClusterCreated)
{ Collector.AddStableReferenceArray(&This->ActorsForGC);
} else
{
Collector.AddStableReferenceArray(&This->Actors);
}
Super::AddReferencedObjects( This, Collector );
}
Console 명령으로 gc.ForceCollectGarbageEveryFrame 1 을 하면 매 프레임 GC 를 수행한다. GC 관련 오류 (변수 설정 잘못 등)가 의심 스러운 타이밍에 입력하면 디버깅에 큰 도움이 된다.