Understanding Garbage Collection in UE5 C++

Practical guide to how Unreal's UObject GC works, common pitfalls, and C++ patterns that keep your game stable and performant.

Overview

Unreal Engine manages memory for engine-managed objects (UObjects) with a built-in garbage collector (GC). Unlike C++ RAII or smart-pointer ownership, UE's GC performs reachability analysis to decide which UObjects are still in use and which can be reclaimed. This article explains the core concepts you need when writing UE5 C++ so you avoid leaks, hitches, and dangling references.


How Unreal's Garbage Collector Works

Mark-and-sweep model

UE uses a mark-and-sweep GC for UObjects: the collector marks reachable objects (roots and anything they reference) and then sweeps away unmarked objects. The marking phase performs reachability analysis across UObject references discovered via the reflection system. This design is deterministic for UObject lifetimes but can cause frame stalls if many objects must be scanned in one frame.

Roots and reachability

Roots include objects referenced from global structures, objects on the stack that the engine knows about, objects added to the root set (e.g., via AddToRoot()), and any references discovered through UPROPERTY metadata. Anything not reachable from the root set is eligible for collection.

Performance note — The marking phase historically runs in a single frame; scanning many objects can produce visible hitches. UE has been adding incremental GC options to reduce pause time in recent engine versions.

UObject lifecycle and reflection

UObjects are created with engine factories (for example, NewObject<T>() or ConstructObject) and are tracked by the GC. The reflection system (UClass, UProperty metadata) is how the GC discovers references between UObjects at runtime; only properties exposed to reflection (typically marked with UPROPERTY) are automatically scanned. Non-reflected raw pointers are invisible to the GC and can lead to premature collection or dangling pointers if not handled carefully.

Common lifecycle operations


References, UPROPERTY, and smart pointers

Understanding which references the GC sees is the single most important concept for safe UE C++ code. Use the reflection system and engine-provided pointer types to express ownership and lifetime correctly.

UPROPERTY

Mark UObject pointers with UPROPERTY() so the GC can find them. Without UPROPERTY, the GC treats the pointer as invisible and may collect the object while your code still holds a raw pointer. Example:

UPROPERTY()
AActor* MyActor;

Pointer types

  • TStrongObjectPtr / UPROPERTY — tracked by GC; use for owning references.
  • TWeakObjectPtr — non-owning, safe to hold; becomes null if object is destroyed.
  • TSharedPtr / std::shared_ptr — not tracked by UE GC; use only for non-UObject data.
  • FGCObject — implement when you need to hold UObject references from non-UObject classes so the GC can be informed.

Example: safe non-owning reference

UPROPERTY()
TWeakObjectPtr<AActor> TargetActor;

if (TargetActor.IsValid())
{
    TargetActor->DoSomething();
}

Best practices for UE5 C++

Tradeoffs

Using UPROPERTY everywhere is safe but increases the amount of data the GC must scan. Minimizing reflected references where possible reduces GC work but requires careful manual lifetime management. Incremental GC reduces pause risk at the cost of implementation complexity and engine version constraints.


Debugging and profiling GC issues

Key tools and techniques:

When investigating hitches, measure how long the marking phase takes and which object sets dominate the scan; that will point to which UPROPERTY graphs to simplify or make non-reflected.