Mikelis' Blog

Memory Management & Garbage Collection in Unreal Engine 5 (Updated)

gc-art

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:

  1. Holding a regular (not soft) reference to a UObject in an instance of a blueprint class.

  2. 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; 
    
  3. Using a raw C++ pointer in a container that's a member field decorated with UPROPERTY:

    UPROPERTY()
    TArray<UObject*> ListOfObjectsWithHardReferences;
    
  4. 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:

  1. 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.
    }
    
  2. 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.

  3. 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:

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:

  1. Building a garbage-collection graph - a tree of objects hard-referenced by something starting at the Root Set,
  2. 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:

Mikelis' Game Devlog

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:

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.

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.

#Cpp #Game Development #Unreal Engine