UObject 메타데이터
FUObjectItem, GUObjectArray, Index 시스템의 구조와 GC에서의 역할을 분석합니다
GUObjectArray 글로벌 객체 배열
모든 UObject를 추적하는 전역 자료구조
GUObjectArray의 구조
GUObjectArray는 FUObjectArray 타입의 전역 변수로, 프로세스 내 모든 UObject의 메타데이터를 저장합니다. GC의 Mark 단계에서 이 배열을 순회하며 Reachability를 분석합니다.
// UObjectArray.h
class FUObjectArray
{
// 청크 기반 할당 (기본 64K 엘리먼트/청크)
enum { NumElementsPerChunk = 65536 };
// 실제 저장소: FUObjectItem 배열들의 배열
FChunkedFixedUObjectArray ObjObjects;
// 여유 인덱스 리스트 (재사용)
TArray<int32> ObjAvailableList;
// 리스너 (생성/삭제 알림)
TArray<FUObjectCreateListener*> UObjectCreateListeners;
TArray<FUObjectDeleteListener*> UObjectDeleteListeners;
public:
// 객체 인덱스로 접근
FUObjectItem* IndexToObject(int32 Index);
// 새 객체 등록
void AllocateUObjectIndex(UObjectBase* Object);
// 객체 해제 (인덱스 재사용 큐에 추가)
void FreeUObjectIndex(UObjectBase* Object);
};
GUObjectArray는 하나의 거대한 연속 배열이 아니라 청크(Chunk) 단위로 분할됩니다. 이를 통해 메모리 재할당 없이 동적 확장이 가능하며, GC 순회 시 캐시 효율을 높입니다. UE5에서는 청크당 65,536개의 슬롯을 사용합니다.
FUObjectItem 구조체
개별 UObject의 메타데이터를 저장하는 핵심 구조체
FUObjectItem의 내부 구조
각 UObject는 GUObjectArray 내에서 FUObjectItem으로 표현됩니다. 이 구조체에는 GC에 필요한 모든 플래그와 메타데이터가 포함되어 있습니다.
struct FUObjectItem
{
// 실제 UObject 포인터
class UObjectBase* Object;
// 플래그 비트필드
int32 Flags;
// 클러스터 인덱스 (Cluster GC용)
int32 ClusterRootIndex;
// 시리얼 번호 (약한 참조 검증용)
int32 SerialNumber;
// 주요 플래그들
enum EFlags
{
EInternalObjectFlags::RootSet = 1 << 0, // GC 루트
EInternalObjectFlags::GarbageCollectionKeepFlags,
EInternalObjectFlags::Unreachable = 1 << 2, // 도달 불가
EInternalObjectFlags::ClusterRoot = 1 << 3, // 클러스터 루트
EInternalObjectFlags::ReachableInCluster = 1 << 4,
EInternalObjectFlags::PendingKill = 1 << 5, // (레거시)
EInternalObjectFlags::Garbage = 1 << 5, // UE5: 가비지 마킹
};
};
SerialNumber의 역할
SerialNumber는 TWeakObjectPtr의 유효성 검사에 사용됩니다. 객체가 파괴되고 같은 인덱스 슬롯에 새 객체가 할당되더라도, SerialNumber가 다르므로 Weak 참조가 잘못된 객체를 가리키지 않습니다.
// TWeakObjectPtr 내부 동작 (간략화)
class FWeakObjectPtr
{
int32 ObjectIndex; // GUObjectArray 내 인덱스
int32 ObjectSerialNumber; // 생성 시점의 SerialNumber
UObject* Get() const
{
FUObjectItem* Item = GUObjectArray.IndexToObject(ObjectIndex);
if (Item && Item->SerialNumber == ObjectSerialNumber)
return Item->Object; // 유효
return nullptr; // 무효 (파괴됨 또는 재사용됨)
}
};
InternalIndex와 오브젝트 플래그
UObject의 식별 체계와 GC 관련 플래그 시스템
InternalIndex 시스템
모든 UObject는 생성 시 GUObjectArray 내의 고유 인덱스(InternalIndex)를 부여받습니다. 이 인덱스는 GC, Weak 참조, 네트워킹 등 다양한 시스템에서 객체 식별에 사용됩니다.
// UObjectBase에 저장된 인덱스
class UObjectBase
{
int32 InternalIndex; // GUObjectArray 내 위치
// 인덱스를 통한 빠른 메타데이터 접근
FUObjectItem* GetUObjectItem() const
{
return GUObjectArray.IndexToObject(InternalIndex);
}
};
// 인덱스 재사용 메커니즘
// 객체 파괴 시: 인덱스가 ObjAvailableList에 추가
// 새 객체 생성 시: ObjAvailableList에서 인덱스를 꺼내 재사용
// 빈 슬롯이 없으면: 배열 끝에 새 슬롯 추가
EObjectFlags (외부 플래그)
| 플래그 | 값 | GC 영향 |
|---|---|---|
RF_NoFlags |
0x0 | 기본 상태, GC 대상 |
RF_Public |
0x01 | 패키지 외부에서 참조 가능 |
RF_Standalone |
0x02 | 외부 참조 없이도 존속 |
RF_Transient |
0x4000 | 직렬화되지 않음, GC는 정상 동작 |
RF_ClassDefaultObject |
0x10 | CDO - Root Set에 포함 |
RF_WasLoaded |
0x80 | 디스크에서 로드됨 |
RF_Standalone 플래그가 설정된 객체는 다른 객체에서 참조하지 않더라도 GC에 의해 수집되지 않습니다. 에디터에서 에셋을 독립적으로 유지하는 데 사용되며, 런타임에서는 주의해서 사용해야 메모리 누수를 방지할 수 있습니다.
GC를 위한 객체 순회 최적화
대량의 UObject를 효율적으로 순회하는 엔진 내부 기법
OpenForDisregardForGC
엔진 초기화 시 생성되는 핵심 객체들은 DisregardForGC 영역에 배치됩니다. 이 영역의 객체들은 GC 순회에서 제외되어 성능을 향상시킵니다.
// 엔진 초기화 시
GUObjectArray.OpenDisregardForGC();
// ... 엔진 핵심 객체 생성 (UClass, CDO 등) ...
GUObjectArray.CloseDisregardForGC();
// 이 시점 이후 생성된 객체만 GC 대상
// DisregardForGC 설정 확인
int32 MaxObjectsNotConsideredByGC;
// DefaultEngine.ini: gc.MaxObjectsNotConsideredByGC
// 기본값: 플랫폼별 상이 (PC: ~약 400,000)
GUObjectArray의 앞부분은 엔진 핵심 객체(UClass, CDO 등)로 채워집니다. CloseDisregardForGC() 이후에 생성된 객체만 GC가 순회하므로, 엔진이 수만 개의 클래스 메타데이터를 매 GC 사이클마다 확인하는 비용을 줄입니다. gc.MaxObjectsNotConsideredByGC로 이 경계를 조정할 수 있습니다.
핵심 요약
- GUObjectArray는 모든 UObject의 메타데이터를 청크 단위로 저장하는 전역 배열입니다
- FUObjectItem에는 GC 플래그, 클러스터 인덱스, SerialNumber 등 GC 핵심 정보가 담겨 있습니다
- SerialNumber는 TWeakObjectPtr의 유효성 검증에 사용되며, 인덱스 재사용 시 안전성을 보장합니다
- EObjectFlags와 EInternalObjectFlags는 GC의 객체 처리 방식을 결정합니다
- DisregardForGC 영역을 통해 엔진 핵심 객체를 GC 순회에서 제외하여 성능을 최적화합니다
도전 과제
배운 내용을 직접 실습해보세요
런타임에서 GUObjectArray를 순회하며 특정 클래스(예: ACharacter)의 모든 인스턴스에 대해 FUObjectItem의 EInternalObjectFlags를 로그에 출력하세요. RootSet, GarbageCollectionKeepFlags 등의 플래그 상태를 확인하세요.
GUObjectArray의 MaxObjectsNotConsideredByGC 값을 확인하고, 대량의 UObject를 생성/삭제하면서 ObjectArrayNum 변화를 stat obj 명령어로 관찰하세요. 인덱스 재사용 패턴을 분석하세요.
FUObjectArray::ForEachObjectOfClass를 활용하여 클래스별 UObject 인스턴스 수와 메모리 사용량을 실시간으로 집계하는 콘솔 명령어를 구현하세요. 에디터 Output Log에서 결과를 확인하세요.