Mikelis' Game Blog

Memory Management & Garbage Collection in Unreal Engine 5

All you need to know to master garbage collection and memory management in your Unreal Engine 5 projects.

Mikelis' Game Devlog

If you are a bit like me, in learning to program for Unreal Engine, you have eventually bumped into the garbage collector (GC). For some, this first encounter is agreeable — it’s a relief that GC looks after us and prevents memory leaks by design. For others, rubbing shoulders with the garbage collector isn’t that great. If we do not play nice with its rules, it will not play nice with our objects. This post is all about running through the basics of being on the right side of the garbage collector.

What is and isn’t Collected

Before we dive into some concrete examples, let’s familiarize ourselves with the core design of the Unreal Engine’s garbage collector. The principal idea is straightforward — the garbage collection mechanism in the engine keeps track of hard-referenced UObjects directly or indirectly by the root set. Once a UObject becomes unreferenced ("unreachable" in the engine's terms), it will be destroyed, and its memory will be freed up. This will happen when GC purges run periodically.

In practice, there are many different ways to create a hard reference to a UObject in Unreal Engine and preventing 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. In some parts of the engine that are quickly being deprecated now, we can still find some memory management with raw pointers to structs. It is not advisable to do our own memory management in Unreal Engine when we have both garbage collection and smart poitners at our disposal.

Advanced Considerations

Even though the core design is simple, there are some implications and concepts that might not be immediately obvious. Here are a few of those:

Listening for UObject Creation and Destruction

Sometimes we have a class that needs to listen to UObject creation and destruction for UObjects that it does not own, nor is associated with. 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 steps:

  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

Seeing this “parent — child” relationship in the graph, one might reasonably think that there must be some logical overlap with Unreal Engine’s outer objects. When a new object is created, we have an opportunity to specify which object is its outer (parent, in a way), or whether our new object should be in the transient package. These relationships are not the same as hard references. Although sometimes a hard reference will be created automatically by specific classes when their outers are what they expect.

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:

Conclusion

In summation, the garbage collection system is a robust part of Unreal Engine that affords C++ programmers a lot of safety from memory leaks, as well as convenience. With this high-level discussion, I was aiming to introduce the system at a conceptual level, and I hope I have achieved that.

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 on Devlog on 11 Jul, 2020. It was reviewed on 16 Oct, 2022.

#Cpp #Engineering #Game Development #Garbage Collection #Longform #Unreal Engine