Quick answer: Outside references prevent GC. Audit with obj refs <actor>. Clear references in GameInstance / save data / cross-level UPROPERTYs before UnloadStreamLevel.

An open-world game streams in regions. After leaving a region, actors are supposed to unload to free memory. Memory profile shows them persisting. Console shows higher actor count than expected.

Diagnose with obj refs

obj refs name=BP_Enemy_C_42

Lists all UObjects holding strong refs. Common culprits:

Fix: Clear External References

void UMyGameInstance::OnLevelStreamingDone(const UWorld* World) {
    for (auto& ActorRef : ActiveEnemies) {
        if (ActorRef.IsValid() && !ActorRef->GetLevel()->IsVisible()) {
            ActorRef = nullptr;
        }
    }
}

Replace strong UPROPERTY refs with TWeakObjectPtr where you don’t need to keep the actor alive across levels.

Latent Unload Pattern

FLatentActionInfo LatentInfo;
LatentInfo.CallbackTarget = this;
LatentInfo.ExecutionFunction = "OnLevelUnloaded";
LatentInfo.UUID = FMath::Rand();
UGameplayStatics::UnloadStreamLevel(this, LevelName, LatentInfo, true);

bShouldBlockOnUnload = true forces the unload to complete before returning. Useful for transitions.

Verify via Memreport

memreport -full
stat levels

Compare actor counts before and after unload. Should drop by the level’s known content. If not, references still exist.

Verifying

Stream a level in, then out. memreport — level memory drops to baseline. obj refs on previously-active actors return empty. Stat levels confirms not-loaded.

“Unloading is GC-driven. Hold no strong references after you don’t need them, and the level unloads cleanly.”

For open-world games especially, use TWeakObjectPtr for any cross-system actor refs — lets GC do its job between regions.