Skip to main content

🗑️ Lesson 2.5: Memory & Garbage Collection

In plain C++ you manage memory by hand. In Unreal, UObjects are garbage-collected — and that changes how you write everything. This lesson explains the rules so you never chase a dangling-pointer crash or a mysterious leak again.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Describe the UObject lifecycle and what triggers garbage collection
  • Explain "reachability" and why UPROPERTY pointers keep objects alive
  • Use TObjectPtr and TWeakObjectPtr appropriately
  • Choose smart pointers (TSharedPtr, TUniquePtr, TWeakPtr) for non-UObject data
  • Know when (rarely) to use raw new/delete

Estimated Time: 60 minutes

Engine Version: Unreal Engine 5.8

In This Lesson

Two Memory Worlds

The single most important distinction: Unreal has two kinds of objects with two memory strategies.

UObjectsEverything else
ExamplesActors, components, subsystems, any UCLASSFString, structs, plain C++ classes, third-party types
Created withNewObject, SpawnActor, CreateDefaultSubobjectnew, stack, or smart pointers
Freed byThe garbage collector automaticallyYou / smart pointers / scope
Keep-alive viaUPROPERTY pointersOwnership (shared/unique ptr, scope)

⚠️ Never delete a UObject

You do not delete actors or components. To remove an actor you call Destroy(); the GC reclaims the memory later. Calling delete on a UObject corrupts the engine's object system. Manual new/delete is reserved for non-UObject data — and even there, smart pointers are usually better.

The UObject Lifecycle

A UObject moves through well-defined phases. For actors specifically, you'll hook into several of these in Module 3:

graph TD A["Created
NewObject / SpawnActor"] --> B["Initialized
constructor runs"] B --> C["BeginPlay
(actors, when play starts)"] C --> D["Alive & ticking"] D --> E["Destroy() / EndPlay
marked for destruction"] E --> F["Unreachable
no UPROPERTY refers to it"] F --> G["Garbage collected
memory reclaimed"]
Figure 1: The lifecycle. Note the gap between "marked for destruction" and "actually collected" — the GC runs periodically, not instantly.

📖 GC runs on a schedule

Garbage collection isn't instant or continuous — it runs at intervals. So after you Destroy() an actor, its memory isn't freed that same frame; it's freed at the next collection once nothing references it. This is why you check pointers for validity rather than assuming an object is gone the instant you asked for its removal.

Reachability & UPROPERTY

The GC uses reachability: starting from a set of "root" objects, it follows every UPROPERTY pointer. Any UObject it can reach is kept; anything it can't reach is destroyed. This is the whole game.

graph LR Root["GC Roots"] --> GM["GameInstance"] GM --> World["World / Levels"] World --> Actor["AEnemy (reachable → kept)"] Actor -->|"UPROPERTY UAmmoData*"| Ammo["UAmmoData (reachable → kept)"] Orphan["UObject with only a raw pointer to it
(unreachable → collected)"]
Figure 2: The GC walks UPROPERTY references from the roots. A UObject reachable only through a non-UPROPERTY pointer is invisible — and gets collected.
UCLASS()
class MYPROJECT_API AEnemy : public AActor
{
    GENERATED_BODY()

    // ✅ Reachable: the GC follows this and keeps the loadout alive.
    UPROPERTY()
    UWeaponData* Loadout;

    // ⚠️ Invisible to GC: the pointed-to object can be destroyed while we
    // still hold this address — a dangling pointer waiting to crash.
    UWeaponData* CachedLoadout;
};

✅ The habit that prevents whole bug classes

Any UObject pointer you store as a member gets a UPROPERTY(). You revisited this in Lesson 2.3 from the specifier angle; here's the mechanism behind it. If you truly want a non-owning reference that's allowed to become null when the object dies, use TWeakObjectPtr (next section) — never a raw pointer.

To validate a pointer before use, Unreal gives you IsValid(), which is safer than a bare null check because it also catches objects pending destruction:

if (IsValid(Target))
{
    Target->TakeDamage(10.0f);   // safe: not null AND not pending kill
}

TObjectPtr & TWeakObjectPtr

Modern Unreal has two wrapper types for UObject references you should know:

TObjectPtr<T> — the modern strong reference

In UE5, Epic recommends declaring UObject pointer members as TObjectPtr<T> instead of raw T*. It behaves like a pointer but gives the engine extra tooling (access tracking, lazy loading support). It's still a UPROPERTY and still keeps the object alive.

// Modern UE5 style for a strong-owned UObject member:
UPROPERTY(VisibleAnywhere)
TObjectPtr<UStaticMeshComponent> Mesh;

// You use it just like a raw pointer:
if (Mesh) { Mesh->SetVisibility(true); }

TWeakObjectPtr<T> — a non-owning reference

Use this when you want to refer to a UObject without keeping it alive, and you're fine with it becoming invalid when the object is destroyed. It never causes a dangling-pointer crash because you must check it before use.

// "I want to remember my last target, but not keep it alive."
UPROPERTY()
TWeakObjectPtr<AActor> LastTarget;

// Access pattern — always resolve and check:
if (AActor* Target = LastTarget.Get())
{
    // Target is valid right now.
}

📖 Strong vs weak — the intent

A strong reference (UPROPERTY() T* or TObjectPtr) says "I own/need this; keep it alive." A weak reference (TWeakObjectPtr) says "I'm just watching; let it die when others are done, and tell me it's gone." Choosing weak for observer-style references prevents accidental object lifetime bloat.

Smart Pointers for Non-UObjects

The GC only manages UObjects. For plain C++ classes and structs (not UCLASS), Unreal provides its own smart pointer family — the equivalent of the standard library's, but Unreal-flavored:

TypeOwnershipAnalogy
TUniquePtr<T>Single owner; frees on destructionstd::unique_ptr
TSharedPtr<T>Shared, reference-countedstd::shared_ptr
TWeakPtr<T>Non-owning view of a TSharedPtrstd::weak_ptr
// A plain (non-UObject) helper class:
class FDamageCalculator { /* ... */ };

// Single owner — freed automatically when the owner is destroyed:
TUniquePtr<FDamageCalculator> Calculator = MakeUnique<FDamageCalculator>();

// Shared ownership when several systems need to hold it:
TSharedPtr<FDamageCalculator> Shared = MakeShared<FDamageCalculator>();
TWeakPtr<FDamageCalculator> Observer = Shared;   // watches without owning

⚠️ Don't cross the streams

Never put a UObject in a TSharedPtr/TUniquePtr, and never manage a non-UObject with UPROPERTY. UObjects → GC (via UPROPERTY/TObjectPtr). Non-UObjects → smart pointers or scope. Mixing the two systems is a reliable way to crash.

✅ When is raw new/delete okay?

Rarely, and only for non-UObjects where you have a clear, contained ownership story — and even then a TUniquePtr is usually cleaner and exception-safe. If you catch yourself typing new UObject-derived, stop: use NewObject or SpawnActor instead.

Hands-on Exercise & Quiz

🏋️ Exercise: Fix the memory bugs

Objective: Spot and correct the memory mistakes in this snippet.

UCLASS()
class MYPROJECT_API ATurret : public AActor
{
    GENERATED_BODY()

    UWeaponData* Weapon;                 // (a)
    TSharedPtr<UWeaponData> SharedWeapon; // (b)
    AActor* CurrentTarget;               // (c) we observe it; it may die

    void Cleanup()
    {
        delete Weapon;                    // (d)
    }
};
✅ Fixes
  • (a) Add UPROPERTY() (or use TObjectPtr) so the GC keeps it alive: UPROPERTY() TObjectPtr<UWeaponData> Weapon;
  • (b) Wrong system — a UObject must not live in a TSharedPtr. Remove it; hold the UObject via UPROPERTY/TObjectPtr.
  • (c) For an observed reference that may die, use TWeakObjectPtr<AActor> (still a UPROPERTY), not a raw pointer.
  • (d) Never delete a UObject. If you own it and want it gone, clear the reference and let the GC collect it (or Destroy() an actor).

🎯 Quick Quiz

Question 1: How does the GC decide a UObject should be kept?

Question 2: You want to reference an actor without keeping it alive, tolerating it being destroyed. Use:

Question 3: How do you correctly manage a plain (non-UObject) C++ helper with single ownership?

Summary

🎉 Key Takeaways

  • Unreal has two memory worlds: UObjects (garbage-collected) and everything else (you/smart pointers/scope). Never mix them.
  • Never delete a UObject — call Destroy() on actors and let the GC reclaim memory on its schedule.
  • The GC keeps objects reachable via UPROPERTY references from roots; store UObject members as UPROPERTY() TObjectPtr<T>.
  • Use TWeakObjectPtr for non-owning references that may become invalid; guard access with IsValid() / .Get().
  • For non-UObjects, use TUniquePtr/TSharedPtr/TWeakPtr; raw new/delete is a last resort.

📚 Additional Resources

🚀 What's Next?

The last foundation piece is the one you'll use every single day: logging, assertions, and debugging — how to see what your code is doing and catch mistakes early with UE_LOG, check, and ensure.

🎉 Lesson complete!

Memory holds no more mysteries. Let's learn to see inside a running game.