언리얼 Garbage Collection = GC
더 이상 사용하지않는 (참조되지않는) 언리얼 오브젝트들을 다음 GC때 메모리를 해제하는 시스템
UnrealEngine.cpp GTimeBetweenPurgingPendingKillObjects = 60 초로 기본값이 세팅되어있다.
마크 - 스윕 방식 (Mark - Sweep)
- Root Set에서 시작
- Root 오브젝트가 참조하는 객체를 찾아 마크(Mark)
- 마크된 객체가 다시 참조하는 객체를 찾아 마크 반복
- GC가 마크되지 않은 객체들을 메모리 해제 (Sweep)
언리얼 오브젝트가 생성되면 GUObjectArray 전역 변수에 저장이 된다.
Root Set 에서부터 시작해 도달할 수 있는 객체들을 전부 마크하고, GUObjectArray 를 순회하며 마크되지 않은 객체들을 메모리 해제한다.

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 |

