Memory Management & Garbage Collection in Unreal Engine 5 (Updated)
Understanding the garbage collector (GC) in Unreal Engine is a big part of game development, and even experienced AAA developers sometimes make GC mistakes. This post is all about running through the basics of object garbage collection in Unreal Engine 5 to help you avoid the common pitfalls.
The Nature of the Beast
Garbage collection is an alternative paradigm to smart pointers and RAII-style memory management. Instead of managing the lifetime of objects yourself, the key idea behind Unreal Engine's garbage collection is that you simply create objects as needed, and so long as they are in use, they will not be deleted.
This naturally leads to a question of what qualifies as "in use", and Unreal Engine has a simple answer: whatever UObject instance is referenced directly or indirectly by the Root Set of objects. This means that any object in the Root Set can itself reference other objects, which will be kept in memory. These objects can reference other objects, and so on.
There are many different ways to create a hard reference to a UObject instance in Unreal Engine and prevent the GC of the referenced object. Here are the common ones:
Holding a regular (not soft) reference to a UObject in an instance of a blueprint class.
Holding a reference to a UObject through a raw C++ pointer declared a class as a member field and decorated with UPROPERTY():
// If this points to an object at runtime, there will be a hard reference between // an instance of this class and that object. UPROPERTY() UObject* HardUObjectReference;
Using a raw C++ pointer in a container that's a member field decorated with UPROPERTY:
UPROPERTY() TArray<UObject*> ListOfObjectsWithHardReferences;
Adding the object/instance to a Root Set, ensuring that this UObject and all other ones with hard references from it will not be garbage collected:
void UMyObject::UMyFunction() { UMyClass* MyObject = NewObject<UMyClass>(this); MyObject->AddToRoot(); // At this point, MyObject will not be garbage collected until it's removed from // the Root Set with RemoveFromRoot(). }
Additionally, there are also ways to hold on to UObject in weak ways that will not prevent their garbage collection. Commonly this is done by:
Passing around a raw C++ pointer in function arguments, or using it in function bodies:
void UMyObject::UMyFunction() { UMyClass* MyObject = NewObject<UMyClass>(this); // At this point, the object pointed to by MyObject can be garbage-collected // practically as soon as UMyFunction() finishes executing. }
Using raw C++ pointers as members in classes and structs - although smarter IDEs like JetBrains Rider will warn us annoyingly that it will be garbage collected as they think we might intend the objects not to be.
Using a weak UObject pointer such as TWeakObjectPtr
or TSoftObjectPointer as a member: // This one can only point to objects in memory. UPROPERTY() TWeakObjectPtr<UMyClass> WeakUObjectReference; // This one can point to objects in memory or unloaded assets, and it's useful in // lazy-loading. UPROPERTY() TSoftObjectPointer<UMyClass> SoftUObjectReference;
Garbage collection only works with UObjects and not other types like structs. Usually, memory management for structs and object instances not derived from the UObject class is handled with the Unreal Smart Pointer Library which provides the basic C++ 11 smart pointer alternatives.
Advanced Tips
Even though the core design of Unreal Engine's GC is simple, there are some practical implications that may not be immediately obvious. Here are a few of those:
Although possible, unique pointers are very rarely used in Unreal Engine game development. Almost all object references in practice work like shared pointers, and trying to use unique pointers as you would in C++ (to imply object ownership, for example) may lead to some Blueprint headaches.
The engine sometimes keeps certain object instances alive through registration that is not immediately visible to the user. For example, the UWorld instance for a world that is loaded (and some vestigial worlds) will always be hard-referenced by the engine. Another example is instances of AActor and USceneComponent in the levels (and indeed, the ULevels themselves in the UWorld). You do not need to have a hard reference to these instances for them to stay alive in most cases.
It is very easy to create large chains of objects all referencing each other in a huge circle. In such case, until every single object in the entire chain is unreachable, the entire chain will stay in memory. This could use very significant memory and you may be surprised to learn that your character, vehicle, weapon or item in game remains in memory even though it's not in use in the game.
TWeakObjectPtr and UPROPERTY-decorated pointers to UObjects will be nulled when they are garbage-collected. Raw C++ pointers will not be nulled as the garbage collector is not aware of them. This is particularly important if you use raw pointers in lambda function callbacks. Be aware that by the time the lambda executes, the pointer may no longer point to an object. It is recommended to use a TWeakObjectPtr<> in that case. Here is some more information about what is and isn't nulled:
// This will be nulled. IsValid(ExampleOne) will return false as soon as the object // this pointer points to starts being GC'd. UPROPERTY() UObject* ExampleOne; // Elements of this will be nulled as they are garbage-collected. IsValid(ExampleTwo[i]) // will likewise return false as soon as the object the element points to starts // being GC'd. UPROPERTY() TArray<UObject*> ExampleTwo; // This will NOT be nulled (except for some very rare, specific and advanced cases). // There is simply no way for the engine to know that this pointer exists by default. // For debugging, testing and development build purposes, we can check this pointer // with ExampleThree->IsValidLowLevel() but this is too unreliable and slow for // shipping code. UObject* ExampleThree; // This will be nulled. ExampleFour.IsValid() will become false as soon as the object // starts being GC'd. ExampleFour.IsStale() will in that case become true. This happens // irregardless of whether the member variable is decorated with UPROPERTY or not. TWeakObjectPtr<UObject> ExampleFour; // This will also be nulled. This happens irregardless of whether the member variable // is decorated with UPROPERTY or not. TSoftObjectPointer<UObject> ExampleFive; // This could be dangerous if the lambda function is called later when MyPointer // no longer points to a live object, but still points to a memory address other // than 0x0. UObject* MyPointer = ... auto MyLambda = [MyPointer]() { ... }
Listening for UObject Creation and Destruction
Although this is not very efficient, you may wish to listen to all UObject creations and destructions. In those cases, we can use the GUObjectArray which is a global symbol of FUObjectArray type that can inform all other objects implementing the FUObjectCreateListener and FUObjectDeleteListener interfaces about all UObject creations and deletions happening in the entire process.
It will generally always be more efficient to use a TWeakObjectPtr<> to understand when a particular object is destroyed, but the following pseudocode example demonstrates doing that without any knowledge of the instance destroyed.
// In .h
UCLASS()
public MYGAME_API AMyActor : public AActor, public FUObjectDeleteListener
{
GENERATED_BODY()
public:
virtual void BeginPlay() override;
virtual void EndPlay(EEndPlayReason::Type EndPlayReason) override;
// Begin FUObjectDeleteListener Interface
virtual void NotifyUObjectDeleted(const UObjectBase* Object, int32 IdxInGUObjectArray) override;
// End FUObjectDeleteListener Interface
};
// In .cpp
void AMyActor::BeginPlay()
{
Super::BeginPlay();
// Start listening for UObject destructions.
GUObjectArray->AddUObjectDeleteListener(this);
}
void AMyActor::EndPlay(EEndPlayReason::Type EndPlayReason)
{
Super::EndPlay(EndPlayReason);
// Stop listening for UObject destructions.
GUObjectArray->RemoveUObjectDeleteListener(this);
}
void AMyActor::NotifyUObjectDeleted(const UObjectBase* Object, int32 IdxInGUObjectArray)
{
// This will be called every time when a UObject is destroyed. It would be wise to not
// put too slow/time-complex/heavy code here. However, as seen below, this could be a
// useful function for debugging.
// Some kind of a debugging assertion could go here.
check(...);
}
Garbage Collection Graph
So far, we have been talking about the practical rules and principles of garbage collection in Unreal. But what really happens in a garbage collection purge?
To give a high-level overview, the purge consists of two main phases:
- Building a garbage-collection graph - a tree of objects hard-referenced by something starting at the Root Set,
- Destroying unreachable/unrefrerenced objects.
Starting at step 1, the engine will start with the Root Set of objects and see what they are hard-referencing and pointing to through the reflection system. Whatever is being pointed to or referenced will be added to the “untouchable list” - the GC graph. Then it will examine what these added objects hard-reference, and it will add all of that to the graph as well. Moving along the graph this way, eventually, the garbage collection system builds a tree of all untouchable objects, and it will just delete everything else.
Here’s a basic illustration of a garbage collection graph:
In recent versions of Unreal Engine (I believe, starting from 5.4) garbage collection has changed slightly. Whereas previously the graph would be built and objects purged immediately (often causing performance issues), this is now done in a more asynchronous way.
Testing Pointers
As briefly mentioned above, we have a series of tools to see if the hard and soft pointers we have point to valid, non-garbage-collected UObjects, and we should do so often in code to provide alternative code paths or throw assertions when our game is executing outside of a safe state.
How we do this depends on the pointer type. For example, here are some assertion code samples for testing various pointers:
- A raw C++ pointer can be tested using IsValid() and IsValidLowLevel():
ensureAlwaysMsgf(IsValid(MyObject), TEXT("Uh oh! MyObject has been garbage collected or will be imminently")); ensureAlwaysMsgf(MyObject->IsValidLowLevel(), TEXT("MyObject pointer seems to point to a memory location which doesn't seem to have a UObject"))
Many new Unreal Engine programmers make the mistake and assume that checking whether a raw C++ pointer is null like if(MyObject)
is always sufficient to know whether the pointer points to a valid UObject. This is not true. For example, the UObject might have already begun destruction, or the pointer might be stale if it hasn't been decorated by UPROPERTY and reflected in Unreal Engine. Use IsValid() - if(IsValid(MyObject))
- for reflected pointers (those member pointers decorated by UPROPERTY or in member containers decorated by UPROPERTY). Use IsValidLowLevel() - if(MyObject->IsValidLowLevel())
- for non-reflected raw C++ pointers, but generally try and move away from non-reflected raw pointers as IsValidLowLevel() is slow.
A TWeakObjectPtr<> can be tested using .IsExplicitlyNull(), .IsValid() and .IsStale():
ensureAlwaysMsgf(!MyObject.IsExplicitlyNull(), TEXT("MyObject was Reset() or never initialized to point to a UObject. In other words, it's == nullptr.")); ensureAlwaysMsgf(MyObject.IsValid(), TEXT("MyObject doesn't point to a valid object.")); ensureAlwaysMsgf(!MyObject.IsStale(), TEXT("MyObject used to point to a UObject, but that UObject has been destroyed."));
A TSoftObjectPointer<> can be tested using .IsNull(), .IsValid() and .IsPending():
ensureAlwaysMsgf(!MyObject.IsNull(), TEXT("MyObject is not initialized or was Reset().")); ensureAlwaysMsgf(MyObject.IsValid(), TEXT("MyObject is not pointing to a UObject in memory, although it might point to an unloaded asset.")); ensureAlwaysMsgf(!MyObject.IsPending(), TEXT("MyObject is not pointing to a UObject in memory, but it may later."));
In some parts of the Engine, and usually you won't be concerned with this, there are bits of code that check the validity of UObjects using simple C++ null checks instead of IsValid(). For example, in blueprint VM functions. If you are working on anything related to blueprints, it is a good idea to check UObject pointer validity before these pointers are ingested into the VM, and it's a good idea to ensure that the objects are never garbage-collected while the VM uses them.
Conclusion
All in all, the garbage collection in Unreal Engine is an alternative to smart pointers for memory management. It is developer-ergonomic, but has certain overheads and performance costs. Moreover, it takes a little bit of effort to learn, and not knowing its rules by heart can mean awkward and difficult-to-debug game crashes during development. Hopefully, this blog post was helpful in illuminating some of these rules.
If you’d like to read more about the Garbage Collection in Unreal Engine, I suggest browsing the Unreal Engine Architecture Documentation as well as Unreal Engine Scripting with C++ Cookbook by William Sherif and Stephen Whittle. The latter recommendation will have a less conceptual overview with a focus on code examples.
An older version of this article was published here on 11 Jul 2020. It was reviewed several times, with the latest revision on 16 July 2025.