Unreal GC — Mark-and-Sweep over the UPROPERTY graph
Unreal's garbage collector is fundamentally different from what a regular C++ programmer expects. There is no reference counting, no std::shared_ptr, no destructor at the moment the last reference disappears. There is a periodic tracing collector: every few seconds the engine stops the world, walks the reachability graph from a root set along fields marked with the UPROPERTY macro, and sweeps everything it did not reach. That's Mark-and-Sweep.
Every other rule flows from that model. A strong reference is a UPROPERTY field holding UObject* or TObjectPtr<>: the GC sees it during tracing and keeps the target alive. A raw UObject* Cached inside a plain C++ class is invisible to the collector: it does not keep the object alive, and after one GC cycle it becomes a dangling pointer. A weak reference TWeakObjectPtr does not keep the object alive at all, but unlike a raw pointer it knows the target has died and honestly answers IsValid() == false.
The third key point: cycles are handled automatically. Two UObjects holding each other via UPROPERTY do not leak: if no path from the root set reaches the cycle, both vertices are unreachable and both are reaped in the same sweep. That's the main advantage of a tracing GC over reference counting, and the reason TWeakObjectPtr in UE is a tool for "non-owning" semantics, not a workaround for cycles the way std::weak_ptr is in the STL.
Topic map
- Object Lifetime —
NewObject, the Outer chain,Destroy,MarkAsGarbage,BeginDestroy,FinishDestroy, GC cycle phases. - GC Algorithm — mark phase, sweep phase, walking the graph through
UPROPERTY, theGUObjectArrayregistry. - GC Root Set — what's in the root set by default,
AddToRoot/RemoveFromRoot, the danger of "permanent" rooting. - Strong References —
UPROPERTY,TObjectPtr<>,TArray<TObjectPtr<>>, auto-nulling when the target dies. - Weak References —
TWeakObjectPtr,IsValid, caches, and back-pointers to an owner. - Validity Check —
IsValidvsIsValidLowLevelvsnullptrcomparison, what each checks and when each lies. - UObject GC — UE GC vs
std::shared_ptr, incremental GC, clustering, what the collector does not manage.
Common traps
| Mistake | Consequence |
|---|---|
Storing UObject* Cached; without UPROPERTY in a C++ class | Dangling pointer after the next GC cycle; crash in a random frame |
Assuming GC works like std::shared_ptr | Expecting reference counting; confusion about why an object is still alive |
Treating a UPROPERTY cycle as a leak | Mark-and-Sweep collects cycles freely; no leak |
Calling AddToRoot "just in case" | Permanent leak: the rooted object and everything it references are never collected |
Comparing to nullptr instead of IsValid | UObject* Ptr is non-null, but the target is already PendingKill — crash on ->Method() |
Calling delete on a UObject | Corrupts the GC registry; crashes and memory corruption |
Using an Actor right after Destroy() | Destroy only marks the object; actual cleanup happens later — a classic trap |
Holding a raw UObject* inside a non-UObject class (FMyStruct, FCallback) | GC cannot see the reference; the object dies, the pointer dangles |
Reaching for TWeakObjectPtr to "break a cycle" | Mark-and-Sweep already breaks cycles; TWeakObjectPtr is for "non-owning" semantics |
Trusting IsValidLowLevel in shipping code | It's diagnostic, not a validator; in release builds it behaves differently and doesn't replace IsValid |
Interview relevance
Garbage collection is mandatory middle-level material for any UE5 interview. Checks:
- That UE's GC is Mark-and-Sweep, not reference counting, and that cycles collect automatically.
- How
UPROPERTYfundamentally differs from a rawUObject*in a C++ class. - What the root set is, and why
AddToRootwithoutRemoveFromRootis a permanent leak. - The difference between
TObjectPtr<>(strong) andTWeakObjectPtr<>(weak). - What
IsValidchecks and why comparing tonullptris not enough. - Why
Destroy()on an Actor is notdelete, and what happens between the two and actual memory release.
Common wrong answer: "Unreal's GC works like smart pointers: as long as someone holds a reference, the object is alive." Wrong twice over — first, the GC is tracing, not counting; second, "someone holds a reference" through a raw UObject* does not exist for the GC. You need a UPROPERTY reference or a root-set entry.