반응형

언리얼 Garbage Collection = GC

더 이상 사용하지않는 (참조되지않는) 언리얼 오브젝트들을 다음 GC때 메모리를 해제하는 시스템

UnrealEngine.cpp GTimeBetweenPurgingPendingKillObjects = 60 초로 기본값이 세팅되어있다.

 

 

마크 - 스윕 방식 (Mark - Sweep)

  • Root Set에서 시작
  • Root 오브젝트가 참조하는 객체를 찾아 마크(Mark)
  • 마크된 객체가 다시 참조하는 객체를 찾아 마크 반복
  • GC가 마크되지 않은 객체들을 메모리 해제 (Sweep)

 

언리얼 오브젝트가 생성되면 GUObjectArray 전역 변수에 저장이 된다.

Root Set 에서부터 시작해 도달할 수 있는 객체들을 전부 마크하고, GUObjectArray 를 순회하며 마크되지 않은 객체들을 메모리 해제한다.

Reference Graph

 

Root Set 으로부터 UObject를 참조중인지 검사를하고 참조되지않으면 GC에서 메모리 해제를 한다.

Root Set 은 GC의 시작점으로 보통 GameInstance 나 로딩된 World , PlayerController 등이 있고, 프로그래머가 직접 AddToRoot 함수를 이용하여 등록시킬수도 있다.

 

UPROPERTY UCLASS 등 매크로를 통하여 언리얼의 리플렉션 시스템을 사용할 수 있다.

이 매크로가 붙은 언리얼 오브젝트들은 GC 메모리 대상이 되어 개발자가 따로 메모리 해제를 해줄 필요가 없다.

 

UPROPERTY 를 명시한 언리얼 오브젝트들은 GC가 메모리를 해제하는것을 막고, 참조되지않을때 다음 GC때 메모리를 해제한다. 

혹은 참조되고 있는 상황이라도 MarkAsGarbage 함수로 마크에서 강제로 다음 GC가 가져가게 할 수 있다.

 

UPROPERTY 가 명시되지않은 언리얼 오브젝트들은 아예 Reference Graph에 참조되지않기 때문에 다른 참조경로가 없으면 다음 GC 실행 때 메모리를 해제한다.

 

nullptr 댕글링 포인터
아무 주소도 가리키지 않음 (0x0) 유효하지 않은 메모리(해제된 메모리)를 카리킴
접근 시, 바로 크래시 크래시가 당장 안 날 수도 있음
디버깅 어려움

 

참고로 GC를 실행하는 명령어 중GEngine->ForceGarbageCollection(true); 는 다음 틱에 GC를 예약하는거고

즉시 정리까지 완료하길 원하면 CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); 함수로 테스트 할 수 있다.

// UPROPERTY TObjectPtr<UObject>
NoOuterObjectPtr = NewObject<UObject>();
UObject* TestNoObjectPtr = NewObject<UObject>(NoOuterObjectPtr);

// UPROPERTY TObjectPtr<UObject>
UObject* TmpOuterObjectPtr = NewObject<UObject>();
SiblingObjectPtr = NewObject<UObject>(TmpOuterObjectPtr);

// UPROPERTY TObjectPtr<UObject>
OuterObjectPtr = NewObject<UObject>(this);
SubObjectPtr = NewObject<UObject>(OuterObjectPtr);
// TWeakObjectPtr<UObject>
OuterWeakTestPtr = OuterObjectPtr;


// UPROPERTY UObject*
OuterRawPtr = NewObject<UObject>(this);
SubRawPtr = NewObject<UObject>(OuterRawPtr);
// TWeakObjectPtr<UObject>
SubWeakTestPtr = SubObjectPtr;


// Outer this
UObject* OuterPtr1 = NewObject<UObject>(this);
OuterWeakPtr1 = OuterPtr1; // TWeakObjectPtr<UObject>
UObject* RefRawPtr1 = NewObject<UObject>(OuterPtr1);
SubWeakPtr1 = RefRawPtr1; // TWeakObjectPtr<UObject>


// Outer 지정 x  Reference 없음
UObject* OuterPtr2 = NewObject<UObject>();
OuterWeakPtr2 = OuterPtr2; // TWeakObjectPtr<UObject>
UObject* RefRawPtr2 = NewObject<UObject>(OuterPtr2);
SubWeakPtr2 = RefRawPtr2; // TWeakObjectPtr<UObject>


// Outer this 
UObject* OuterPtr3 = NewObject<UObject>(this);
OuterWeakPtr3 = OuterPtr3; // TWeakObjectPtr<UObject>
UObject* RefRawPtr3 = NewObject<UObject>(OuterPtr3);
SubWeakPtr3 = RefRawPtr3; // TWeakObjectPtr<UObject>

// UPROPERTY TArray<UObject*> 에 넣음
ObjectArray.Add(OuterPtr3);

// this 의 Outer 변경 
UObject* abc = NewObject<UObject>();
Rename(nullptr, abc);

// true
if (OuterWeakPtr1.IsValid()) { UE_LOG(LogTemp, Warning, TEXT("Test")); }
// true
if (OuterWeakPtr2.IsValid()) { UE_LOG(LogTemp, Warning, TEXT("Test")); }
// true
if (OuterWeakPtr3.IsValid()) { UE_LOG(LogTemp, Warning, TEXT("Test")); }

// 강제 GC 실행
CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS);

// false
if (OuterWeakPtr1.IsValid()) { UE_LOG(LogTemp, Warning, TEXT("Test")); }
// false
if (OuterWeakPtr2.IsValid()) { UE_LOG(LogTemp, Warning, TEXT("Test")); }
// true -> ObjectArray가 참조
if (OuterWeakPtr3.IsValid()) { UE_LOG(LogTemp, Warning, TEXT("Test")); }

 

강제 GC 실행 후 각 포인터에대한 값을 확인해보면 다음과 같음을 확인할 수 있다.

NoOuterObjectPtr
살아있음 UPROPERTY 가 붙여져 있고 this가 멤버변수로 참조하여 제거하지 않는다
TestNoObjectPtr 댕글링 포인터 UPROPERTY 가 아니어서 GC가 메모리 해제함
     
TmpOuterObjectPtr 살아있음 SiblingObjectPtr 로부터 Outer 설정됨 
SiblingObjectPtr 살아있음 UPROPERTY 가 붙여져 있고 this가 멤버변수로 참조하여 제거하지 않는다
     
OuterObjectPtr  살아있음 UPROPERTY 가 붙여져 있고 this가 멤버변수로 참조하여 제거하지 않는다
SubObjectPtr 살아있음 UPROPERTY 가 붙여져 있고 this가 멤버변수로 참조하여 제거하지 않는다
     
OuterRawPtr 살아있음 UPROPERTY 가 붙여져 있고 this가 멤버변수로 참조하여 제거하지 않는다
SubRawPtr 살아있음 UPROPERTY 가 붙여져 있고 this가 멤버변수로 참조하여 제거하지 않는다
     
OuterPtr1 댕글링 포인터  (Name = "None" 혹은 illegal Name) 참조되지 않아 GC가 메모리 해제
RefRawPtr1 댕글링 포인터 (Name = "None" 혹은 illegal Name) 참조되지 않아 GC가 메모리 해제
     
OuterPtr2 댕글링 포인터 (Name = "None" 혹은 illegal Name) 참조되지 않아 GC가 메모리 해제
RefRawPtr2 댕글링 포인터 (Name = "None" 혹은 illegal Name) 참조되지 않아 GC가 메모리 해제
     
OuterPtr3 살아 있음 UPROPERTY TArray<UObject*> 멤버변수에 넣어놨기때문에 GC가 메모리 해제하지 않음
RefRawPtr3 댕글링 포인터 (Name = "None" 혹은 illegal Name) 참조되지 않아 GC가 메모리 해제
     
abc 살아 있음 this 로부터 Outer 설정됨

Outer 는 SubObject 의 GC 를 막지 않는다(Referencing 하지 않는다). 오히려 SubObject 로부터 Outer 로 Referencing 이 걸린다.

 

Outer에 대한 설명은 코드 주석엔 이렇게 되어있다.

The object to create this object within - 이 객체를 생성할 때 포함시킬 대상 객체입니다.

 

그림으로 이해하면 

OuterObject 가 SubObject를 참조하는게 아니라 SubObject가 OuterObject를 참조하는 것이다.

그래서 다음 GC 가 돌면 Root Set 으로 부터 참조될 수 없는 UObject들이 제거된다.

 

 

실전에서 쓰인다고 했을 때

UCLASS()
class TEST_API UTestOuterObject : public UObject
{
	GENERATED_BODY()
public:
    UPROPERTY()
    TArray<UObject*> TestArray;
};




UPROPERTY()
UTestOuterObject* TestOuterObject;
UPROPERTY()
TArray<TWeakObjectPtr<UObject>> TestWeakObjectArray;


TestOuterObject = NewObject<UTestOuterObject>(this);
for(int i=0; i<CreateCount; ++i)
{
    UObject* a = NewObject<UObject>(TestOuterObject);
    TestOuterObject->TestArray.Add(a);
    TestWeakObjectArray.Add(a);
}

// Another Function
TestOuterObject->MarkAsGarbage();
CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS);

 

TWeakObjectPtr은 레퍼런스 카운트를 증가시키지 않기 때문에 GC 대상인지 확인할때 테스트 하기도 용이하다.

단, UTestOuterObject / TestArray / a123 모두 어디선가 참조하는곳이 있으면 GC가 가져가지 않는다.

 

 

참고로 위젯 예를들어 WrapBox나 UniformGridPanel 에 넣고 MarkAsGarbage 시켜도 캔버스 패널에서 참조하고있어 GC가 되는지 확인할 수 없다.

 

 

 

 

TWeakObjectPtr 를 사용해야 하는 이유?

다음과 같이 참조 관계일 때 더 이상 사용하지 않을 오브젝트들을 GC에서 가져가는것을 기대해야하는데 약한참조는 레퍼런스 카운트로 치지 않아 Outer가 사라지면 같이 사라지는것을 확인할 수 있다.

 

 

그리고 하드 레퍼런싱으로 참조 하였을때 테스트 결과

TArray 원소 하나를 하드 레퍼런싱 하였고, UObject1 을 제외한 UObject2,3 는 GC에서 가져갈거라 기대하였는데 모두 가져가지 않았다.

 

TestOuterObject 0x000007fa5c39d3e0 (Name="TestOuterObject"_0, InternalFlags=2097156)

TestArray의 첫번째 원소의 outerPrivate

-OuterPrivate {ObjectPtr=0x000007fa5c39d3e0 (Name="TestOuterObject"_0, InternalFlags=2097156) }

 

 

TestOuterObject 는 NULL

TestArray의 첫번째 원소의 outerPrivate

-OuterPrivate {ObjectPtr=0x000007fa5c39d3e0 (Name="TestOuterObject"_0, InternalFlags=2097154) }

 

이 부분은 GPT가 특히, MarkAsGarbage()를 수동 호출하거나 GC가 해당 오브젝트를 살려야 할 필요가 없다고 판단한 경우, 일부 플래그(RF_Native, EInternalObjectFlags::Native)는 자동으로 제거될 수 있습니다. 라는데 조금더 확인이 필요

반응형

'언리얼' 카테고리의 다른 글

World Widget Depth Test  (0) 2025.09.24
Custom Depth Stencil Pass  (0) 2025.09.19
FAB 플러그인 등록 과정  (1) 2025.04.24
모바일 EnableGestureRecognizer 관련  (0) 2025.01.02
AsyncTask 간단 예제  (0) 2024.12.16

+ Recent posts