ue-memory-and-gc
Manages memory and garbage collection in Unreal Engine so objects stay alive when you need them and get cleaned up when you don't.
Installation
Paste this into Claude Code, Cursor, or any agent that can run commands.
SKILL.mdShow the author's original SKILL.md
---
name: ue-memory-and-gc
description: Manage UObject lifetime and plain C++ memory in Unreal Engine. Covers the garbage
collector reachability cycle and root set, keeping UObjects alive with UPROPERTY and TObjectPtr,
non-owning TWeakObjectPtr, path-only TSoftObjectPtr, TStrongObjectPtr for non-UObject owners,
FGCObject::AddReferencedObjects, AddToRoot/RemoveFromRoot, MarkAsGarbage and IsValid checks,
and the non-UObject smart pointers TSharedPtr/TSharedRef/TWeakPtr/TUniquePtr and MakeShared.
Use when choosing a pointer or ownership type, debugging crashes after garbage collection,
investigating dangling pointer or use-after-free bugs, holding UObjects from non-UObject
classes, or picking between TSharedPtr and TUniquePtr for plain C++ objects.
metadata:
engine-version: "5.8"
category: cpp-foundations
---
# Memory & garbage collection
Unreal has **two** memory worlds: garbage-collected `UObject`s managed by the engine's GC, and
ordinary C++ heap objects you manage with smart pointers or RAII. Mixing them up causes most UE
crashes. The one rule that prevents the majority of them: **a UObject stays alive only while
something reachable from the root set holds it in a `UPROPERTY` (or an equivalent GC-tracked
reference).**
## When to use this skill
- Choosing which pointer type to use for a UObject or plain C++ object.
- Crash after a few seconds or after a level load — the classic "GC collected it" symptom.
- A non-UObject class (subsystem helper, Slate widget, RAII guard) needs to keep a UObject alive.
- "Why is my pointer suddenly null or garbage?" debugging.
- Deciding between `TSharedPtr`, `TSharedRef`, `TUniquePtr`, or `FGCObject` for a new type.
See also: `ue-cpp-fundamentals` for UObject/UPROPERTY/UFUNCTION macro basics; `ue-actors-and-components`
for actor ownership and component lifetime.
## How UObject GC works
The garbage collector runs periodically (between frames by default). It marks every UObject
**reachable** from the *root set* by following `UPROPERTY` references and a small number of other
tracked reference types. Anything not reachable is collected: `BeginDestroy` is called, then
`FinishDestroy`, then the memory is reclaimed.
**Root set** — the starting nodes. In practice: the global `UEngine`, loaded `UWorld`s, all
`UPackage`s, objects explicitly added via `AddToRoot()`, and objects held by `FGCObject` /
`TStrongObjectPtr` reporters. From these, the GC follows every `UPROPERTY()` transitively.
**What the GC cannot see:**
- A raw `UObject*` member that is *not* a `UPROPERTY` — invisible, can be collected while your
pointer still points at freed memory.
- A `TSharedPtr<UMyObject>` — the smart pointer's ref-count is unknown to the GC; don't do it.
- A `UObject*` captured in a lambda by raw pointer — ditto.
**GC clustering** groups subobjects together so the whole cluster is reclaimed at once, reducing
overhead. Configurable under *Project Settings → Garbage Collection*.
**Incremental GC** spreads the mark pass across multiple frames to reduce hitches; incremental
reachability analysis is opt-in via `gc.AllowIncrementalReachability` (incremental purge is on by
default). `TObjectPtr` members enable GC write barriers that make incremental marking safe.
Full reachability cycle and destruction callbacks:
[references/uobject-gc-and-roots.md](references/uobject-gc-and-roots.md).
## Pointer types — which to use when
| Type | GC-visible? | Keeps alive? | Notes |
|---|---|---|---|
| `UPROPERTY() TObjectPtr<T>` | yes | yes | owned UObject member — modern default |
| `UPROPERTY() T*` (raw) | yes | yes | legacy, still valid; prefer `TObjectPtr` in UE5 |
| raw `T*` without `UPROPERTY` | no | no | locals, params, short-lived; never store as a member |
| `TWeakObjectPtr<T>` | yes (auto-nulled) | no | non-owning; check before use |
| `TSoftObjectPtr<T>` | path only | no | asset loaded on demand; stores disk path |
| `TStrongObjectPtr<T>` | yes (ref-count) | yes | non-UObject owner; no `UPROPERTY` needed |
| `TSharedPtr<T>` / `TUniquePtr<T>` | no | yes (ref/scope) | plain C++ (`F*`) only; never wrap UObjects |
Full pointer-type mechanics and source references:
[references/object-pointer-types.md](references/object-pointer-types.md).
## The UObject-GC pattern — owned members
```cpp
UCLASS()
class MYGAME_API UInventory : public UObject
{
GENERATED_BODY()
// GC-tracked, editor-visible owned reference — the right default
UPROPERTY(VisibleAnywhere)
TObjectPtr<UItemData> ActiveItem;
// Cross-system reference that may disappear independently
TWeakObjectPtr<AActor> LastInteractedActor;
// Asset loaded on demand (stores a path, not a live pointer)
UPROPERTY(EditAnywhere)
TSoftObjectPtr<UStaticMesh> PreviewMesh;
};
```
- `TObjectPtr` in a `UPROPERTY` enables the GC write barrier required for incremental GC marking.
Prefer it over raw `T*` for all UObject members in UE5.
- `TWeakObjectPtr` is **not** a `UPROPERTY` here intentionally — it is a non-owning observer. If
you add `UPROPERTY()` to it, the GC will still not keep the target alive (weak semantics are
preserved), but the pointer *will* be serialized. Omitting `UPROPERTY` is the common pattern for
transient cross-system caches.
- A `TSoftObjectPtr` holds only a path; call `TSoftObjectPtr::LoadSynchronous()` or use
`FStreamableManager` to actually load. See `ue-asset-management` for async loading.
## Validity checks
```cpp
// Prefer IsValid() over bare null checks for UObjects
if (IsValid(MyObj)) // non-null AND not marked as garbage
{
MyObj->DoSomething();
}
// Weak pointer: always go through Get() or IsValid()
if (UMyThing* T = WeakThing.Get()) // returns null if target was GC'd
{
T->Act();
}
// Pin a weak ptr to a strong ref for the duration of a worker thread op
TStrongObjectPtr<UMyThing> Pinned = WeakThing.Pin();
if (Pinned)
{
// Pinned keeps the object alive while this scope runs
}
```
`IsValid` is declared in `Runtime/CoreUObject/Public/UObject/Object.h`:1886.
`TWeakObjectPtr` resolves to null automatically after the target is collected — unlike a raw
pointer, which becomes a dangling address.
## Holding UObjects from non-UObject classes
A plain `F*` class cannot use `UPROPERTY`. Two options:
```cpp
// Option A: FGCObject — report references to the GC explicitly
class FMyManager : public FGCObject
{
public:
virtual void AddReferencedObjects(FReferenceCollector& Collector) override
{
Collector.AddReferencedObject(OwnedData); // keeps OwnedData alive
}
virtual FString GetReferencerName() const override
{
return TEXT("FMyManager");
}
private:
TObjectPtr<UMyData> OwnedData;
};
// Option B: TStrongObjectPtr — simplest for one object
TStrongObjectPtr<UMyData> Held(NewObject<UMyData>());
// Held keeps the object alive until Held goes out of scope or is reset
```
`FGCObject` is declared in
`Runtime/CoreUObject/Public/UObject/GCObject.h`:127.
`TStrongObjectPtr` is declared in
`Runtime/Core/Public/UObject/StrongObjectPtrTemplates.h`:25.
**Important:** `TStrongObjectPtr` cannot be used inside a `UCLASS` as a `UPROPERTY`; using it
inside a UObject without `UPROPERTY` creates cycles that the GC cannot collect. Reserve it for
genuinely non-UObject owners (subsystem helpers, RAII guards, test fixtures).
`FGCObject` instances are **not trivially relocatable** — don't put them in `TArray` by value.
See the `TIsTriviallyRelocatable` specialization in `GCObject.h`:226.
## Creating, destroying, and the root set
```cpp
// Create a UObject (outside of a constructor)
UMyData* Data = NewObject<UMyData>(this); // 'this' becomes the outer
// Constructor-only — only call from an AActor/UActorComponent constructor
UMyComp* Comp = CreateDefaultSubobject<UMyComp>(TEXT("MyComp"));
// Force-keep alive globally (use sparingly — easy to leak permanently)
Data->AddToRoot();
// ... later, must pair with:
Data->RemoveFromRoot();
// Request destruction (for non-actors)
Data->MarkAsGarbage(); // GC collects on the next pass; do NOT 'delete' UObjects
// Actors: use Destroy() instead
MyActor->Destroy();
```
`AddToRoot`/`RemoveFromRoot`/`MarkAsGarbage` are declared in
`Runtime/CoreUObject/Public/UObject/UObjectBaseUtility.h`:231, 237, 207.
`NewObject` overloads are declared in
`Runtime/CoreUObject/Public/UObject/UObjectGlobals.h`:1931.
`CollectGarbage` / `TryCollectGarbage` (force a GC pass) are declared in
`Runtime/CoreUObject/Public/UObject/UObjectGlobals.h`:952.
## Non-UObject memory — plain C++ smart pointers
These are for `F*` structs, engine subsystems, and anything that is *not* a UObject. Never wrap a
UObject in a `TSharedPtr` — the reference count is invisible to the GC and the two ownership
models fight each other.
```cpp
// TUniquePtr — sole owner, destructs when it goes out of scope
TUniquePtr<FMyConfig> Config = MakeUnique<FMyConfig>(/* args */);
// TSharedPtr — shared ownership; released when last owner is gone
TSharedPtr<FConnectionState> State = MakeShared<FConnectionState>();
// TSharedRef — non-nullable shared; ideal for APIs that must always have a value
TSharedRef<FMyService> Service = MakeShared<FMyService>();
// TWeakPtr — non-owning observer; check IsValid() / Pin() before use
TWeakPtr<FConnectionState> WeakState = State;
if (TSharedPtr<FConnectionState> Pinned = WeakState.Pin())
{
Pinned->Send();
}
```
`TSharedPtr`/`TSharedRef`/`TWeakPtr` are declared in
`Runtime/Core/Public/Templates/SharedPointer.h`.
`TUniquePtr` is declared in
`Runtime/Core/Public/Templates/UniquePtr.h`.
Full smart-pointer guide with thread-safety notes and `TSharedFromThis`:
[references/smart-pointers.md](references/smart-pointers.md).
## Gotchas
- **Raw `UObject*` member without `UPROPERTY`** → collected → dangling crash. This is the #1 UE
memory bug. Always `UPROPERTY()` a stored UObject pointer.
- **`TSharedPtr` around a UObject** → ref-count invisible to GC; the GC will collect the object
while the shared pointer still holds an address → crash. Never do it.
- **Storing a strong ref to an actor in a global or long-lived system** can prevent level unloads.
Prefer `TWeakObjectPtr` for cross-system or cross-level actor references.
- **`Weak.Get()` / `Weak.IsValid()` without a check** after the target was collected → null deref.
- **`AddToRoot` without a paired `RemoveFromRoot`** → permanent leak; the object never collects.
- **`TStrongObjectPtr` inside a `UCLASS` member without `UPROPERTY`** → uncollectable cycle; the
object keeps itself alive even when nothing else references it.
- **`MarkAsGarbage` on a rooted object** → asserts (`check(!IsRooted())`); remove from root first.
- **Calling `delete` on a UObject** → double-free; let GC own destruction.
- **`FGCObject` stored by value in a `TArray`** → relocates the instance, breaking GC registration
(not trivially relocatable). Store by pointer or use another lifetime strategy.
## Version notes
- `MarkAsGarbage()` replaced `MarkPendingKill()` in UE5. If `gc.PendingKillEnabled=false`
(default in UE5), auto-nulling of `UPROPERTY` pointers on the target object no longer happens;
use `IsValid()` checks and clear pointers manually in `OnDestroyed`/`EndPlay`.
- `TObjectPtr<T>` is the UE5 idiom for UObject `UPROPERTY` members. Older codebases use raw `T*
UPROPERTY`, which still compiles and works, but `TObjectPtr` adds the GC write barrier needed
for incremental GC and cook-time dependency tracking.
- `TLazyObjectPtr` is slated for deprecation; migrate to `TSoftObjectPtr`.
- `FGCObject::EFlags::RegisterLater` (UE 5.4+) allows deferred GC registration for
partially-initialized objects; register explicitly with `RegisterGCObject()`.
## References & source material
Engine source (UE 5.8, under `Engine/Source/`):
- `Runtime/CoreUObject/Public/UObject/ObjectPtr.h` — `TObjectPtr<T>` / `FObjectPtr`.
- `Runtime/CoreUObject/Public/UObject/WeakObjectPtr.h` — `TWeakObjectPtr<T>`, `FWeakObjectPtr`.
- `Runtime/Core/Public/UObject/StrongObjectPtrTemplates.h` — `TStrongObjectPtr<T>`:25.
- `Runtime/CoreUObject/Public/UObject/GCObject.h` — `FGCObject`:127,
`AddReferencedObjects`:195, `GetReferencerName`:198.
- `Runtime/CoreUObject/Public/UObject/UObjectBaseUtility.h` — `MarkAsGarbage`:207,
`AddToRoot`:231, `RemoveFromRoot`:237.
- `Runtime/CoreUObject/Public/UObject/Object.h` — `IsValid`:1886.
- `Runtime/CoreUObject/Public/UObject/UObjectGlobals.h` — `NewObject`:1931,
`CollectGarbage`:952, `TryCollectGarbage`:962.
- `Runtime/CoreUObject/Public/UObject/SoftObjectPtr.h` — `TSoftObjectPtr<T>`.
- `Runtime/CoreUObject/Public/UObject/GarbageCollection.h` — GC internals, `CollectGarbage` flags.
- `Runtime/Core/Public/Templates/SharedPointer.h` — `TSharedPtr`/`TSharedRef`/`TWeakPtr`.
- `Runtime/Core/Public/Templates/UniquePtr.h` — `TUniquePtr`.
Official docs (UE 5.8, all verified live):
- Object Pointers —
<https://dev.epicgames.com/documentation/unreal-engine/object-pointers-in-unreal-engine>
- Unreal Object Handling —
<https://dev.epicgames.com/documentation/unreal-engine/unreal-object-handling-in-unreal-engine>
- Objects (UObject) —
<https://dev.epicgames.com/documentation/unreal-engine/objects-in-unreal-engine>
- UObject Instance Creation —
<https://dev.epicgames.com/documentation/unreal-engine/creating-objects-in-unreal-engine>
- Unreal Smart Pointer Library —
<https://dev.epicgames.com/documentation/unreal-engine/smart-pointers-in-unreal-engine>
Deep-dive references in this skill:
- [references/uobject-gc-and-roots.md](references/uobject-gc-and-roots.md) — GC reachability
cycle, root set, `BeginDestroy`/`FinishDestroy`, incremental GC, destruction callbacks.
- [references/object-pointer-types.md](references/object-pointer-types.md) — all UObject pointer
types in depth: `TObjectPtr`, `TWeakObjectPtr`, `TSoftObjectPtr`, `TStrongObjectPtr`, and the
rules for when to use each.
- [references/smart-pointers.md](references/smart-pointers.md) — non-UObject smart pointers:
`TSharedPtr`/`TSharedRef`/`TWeakPtr`/`TUniquePtr`, `MakeShared`, `TSharedFromThis`, thread
safety, and `FGCObject` vs `TStrongObjectPtr` selection.
Ships with 3 supporting files:
- references/object-pointer-types.md
- references/smart-pointers.md
- references/uobject-gc-and-roots.md
Mirrored from the author's public source. Install counts from the open skills registry.