垃圾回收与对象生命周期
Garbage Collection & Object Lifecycle
UE 用标记-清除式的可达性分析管理 UObject 内存:从根集合 (Root Set) 出发沿反射可见的强引用遍历对象图,找不到的对象在下次垃圾回收 (Garbage Collection, GC) 时被删除。因此“谁持有 UPROPERTY 引用”决定了对象活多久;Actor 调用 Destroy() 只是请求销毁,真正释放仍取决于引用是否全部放开。
原理:从根集合出发的可达性分析
引擎构建一张引用图,图根是一组被指定为根集合的 UObject(任意 UObject 都可加入根集合)。GC 时从根集合沿已知引用搜索,未被搜到的对象视为不再需要并被移除。GC 在帧与帧之间运行。
下图示意一次 GC 中哪些对象存活:实线是反射可见的强引用,虚线是不被 GC 识别的引用。
flowchart TD Root["根集合"] --> Level["关卡 Level"] Level --> A["Actor"] A --> C1["组件 (Actor 持有)"] A -->|"UPROPERTY 指针"| O1["UObject A:存活"] A -.->|"原生裸指针"| O2["UObject B:无其他强引用 → 被回收<br/>裸指针变为悬空"] A -.->|"TWeakObjectPtr"| O3["UObject C:无强引用 → 被回收<br/>弱指针自动失效"]
哪些引用能让对象存活
| 引用方式 | 阻止 GC? | 对象销毁后 |
|---|---|---|
UPROPERTY() 对象指针 / TObjectPtr | 是 | Actor/组件销毁时自动置空 |
引擎容器中的对象指针(如 UPROPERTY() TArray<UObject*>) | 是 | 同上 |
TStrongObjectPtr | 是 | — |
TWeakObjectPtr | 否 | 自动失效,可先查询有效性 |
| 原生裸指针(无 UPROPERTY) | 否 | 不会置空,悬空指针 |
实践含义:想让对象活着,通常就要保留一个 UPROPERTY 引用。Actor 和组件常是例外——Actor 被所属关卡引用(关卡连回根集合),组件被所属 Actor 引用。
对象生命周期
下图是 Actor 从创建到内存释放的主要阶段(依据文档中构造、BeginPlay、Destroy、GC 的描述整理)。
flowchart LR N["NewObject / SpawnActor<br/>内存清零 → 默认构造函数"] --> B["BeginPlay<br/>注册 Tick,初始化逻辑"] B --> P["游戏中运行"] P --> D["Destroy() / DestroyComponent()<br/>移出关卡,反射引用置空"] D --> W["等待所有强引用释放"] W --> G["GC 可达性分析 → 删除"]
- 构造阶段只设默认值和子对象(见 UObject 与反射系统)。
- 移除 Actor 的标准做法是
Destroy();组件可用DestroyComponent(),但通常随所属 Actor 一起销毁。 - 即使 Actor 已
Destroy()并移出关卡,只要还有强引用,它就不会被回收。
MarkPendingKill 的变化(UE5)
MarkPendingKill()已被MarkAsGarbage()取代,后者现在主要用于追踪“陈旧”对象。只有当gc.PendingKillEnabled=true时,被标记的对象才会被自动置空并销毁。- 若该模式关闭,
MarkAsGarbage只是告诉所有者“希望销毁”,对象仍要等所有引用释放才会回收。用MarkPendingKill()强制回收昂贵对象的做法不再有效。 - 原本依赖 GC 自动清空指针的空指针检查,应改为
IsValid()(同时检查空与垃圾)。但文档也指出,多数IsValid可以用更好的约定替代,例如在 Actor 的销毁事件里主动清空指向它的指针。 - 若不希望引用延长寿命:改用弱指针,或用由程序员手动清空的普通指针(性能关键时)。因为 GC 只在帧间运行,可在一次游戏操作中把弱指针解引用一次后使用。
收集器内部:四个阶段
UE 的 GC 是标准的标记-清除 (Mark & Sweep) 收集器,分四个阶段:标记不可达 (Mark Unreachable) → 标记可达 (Mark Reachable) → 清除 (Sweep) → 收缩哈希表 (Shrink Hash Table)。(Garbage Collector Internals (Epic KB, Ari Arnbjörnsson, Staff),Epic 员工知识库文章,非版本化文档)
| 阶段 | 做什么 | 成本要点 |
|---|---|---|
| 标记不可达 | 先把所有 UObject 视为不可达 | 5.4 起改为一次标志位交换,很快;之前要 ParallelFor 遍历所有对象 |
| 标记可达 | 从根对象出发并行递归跟随所有 UPROPERTY,并调用 AddReferencedObjects | 通常最耗时,与 UObject 数量强相关;粗略超过约 10 万对象时考虑削减或用 GC 簇 |
| 清除 | IncrementalPurgeGarbage 增量销毁仍不可达的对象 | 关卡流出可能强制全量清除,可用 s.ForceGCAfterLevelStreamedOut 0 关闭(世界分区关卡自动关闭) |
| 收缩哈希表 | 全部清除后压缩按类型查找对象的哈希表 | 日志 Compacting FUObjectHashTables data took |
(以上 Garbage Collector Internals (Epic KB, Ari Arnbjörnsson, Staff))诊断:log LogGarbage verbose 输出各阶段耗时,gc.DumpAnalyticsToLog 1 输出分析数据。
被销毁对象依次收到:BeginDestroy(释放内存与多线程资源,如渲染线程代理;玩法清理应在更早的 EndPlay 做)→ IsReadyForFinishDestroy(返回 false 可推迟到下一次 GC)→ FinishDestroy(释放内存前最后一次调用)。GC 会把对象及其子对象组成簇 (Cluster) 一起释放以减少耗时与内存抖动;项目设置里可关闭「Create Garbage Collector UObject Clusters」,但多数项目会更慢,只在性能测试证明有益时才关。(Actor Lifecycle)
垃圾消除 (Garbage Elimination) 开关:默认情况下,被 Destroy() 等标记为 Garbage 的对象即使仍被引用也会被回收,GC 会把指向它的 UPROPERTY 置空。UE5 可设 gc.GarbageEliminationEnabled=false(旧名 gc.PendingKillEnabled),此时只回收没有引用的对象:IsValid() 仍返回 false,但必须自己清空 C++ 强引用(蓝图引用仍由 GC 清除)才能释放内存。文章称 Lyra 已关闭此功能,并预计在某个 UE6 版本中默认关闭。(Garbage Collector Internals (Epic KB, Ari Arnbjörnsson, Staff))这与上面「MarkPendingKill 的变化」一致:强引用不该被别人从底下置空。
增量可达性分析(实验性,UE 5.7 文档)
可达性分析默认要在一帧内完成,期间暂停所有 UObject 处理,对象越多卡顿越明显;传统规避手段是严格的 UObject 预算、对象池或游戏中禁用 GC。增量可达性分析把这一阶段拆到多帧,按每帧软时间上限执行;跨帧期间靠 TObjectPtr 属性追踪引用变化:对 TObjectPtr 型 UPROPERTY 的赋值会立即把对象标为可达,即 GC 写屏障 (Write Barrier)。因此项目里所有 UPROPERTY 都必须从裸指针改为 TObjectPtr,否则对象可能被过早回收。(Incremental Garbage Collection)
[ConsoleVariables]
gc.AllowIncrementalReachability=1
gc.AllowIncrementalGather=1
gc.IncrementalReachabilityTimeLimit=0.002
已知限制:功能并非完全线程安全,在工作线程上被操作的对象可能没被标记为可达而被过早回收,官方建议只在单线程构建(如专用服务器)中启用。调试可用 gc.VerifyNoUnreachableObjects、gc.DelayReachabilityIteration。(Incremental Garbage Collection)
实际项目中的 GC 调优
项目设置 Engine → Garbage Collection 中的常用项:
| 设置 | 作用 | 取舍 |
|---|---|---|
| 创建 GC UObject 簇 (Clusters)(默认开) | 相关对象分组,可达性分析只检查整簇 | 簇内对象在同一帧被反哈希并准备删除,簇大时可能卡顿 |
| 合并 GC 簇 | 跨簇引用时簇合并 | 合并后清除引用不会拆簇;簇中任一对象被引用就让整簇存活,可能该回收的不回收 |
| Actor 簇 | 对 bCanBeInCluster/CanBeInCluster() 为真的 Actor 成簇;默认只有静态网格 Actor 和反射捕获组件开启 | 适合整体一起销毁的对象,如随子关卡卸载的静态网格 |
| 蓝图簇 | 把蓝图生成类及共享的属性/函数数据成簇 | 针对类本身,不是蓝图实例 |
| 清除待删除对象的间隔 | 调整 GC 频率 | 缩短间隔使每次发现的不可达对象更少,避免大量 Actor 同时清理引起的卡顿 |
GC 本身支持多线程可达性分析,并对从容器中移除 Actor 的反哈希做了优化。另外,构建目标选项 bRequireObjectPtrForAddReferencedObjects 要求 FReferenceCollector API 使用 TObjectPtr,文档注明这是为兼容增量 GC。
常见坑
- 把 UObject 存在没有
UPROPERTY的成员或普通 C++ 容器里:GC 看不到,对象会被回收,留下悬空指针;这是最常见的随机崩溃来源之一。 - 反过来,长期持有强引用会让本该消失的对象(包括已 Destroy 的 Actor)一直留在内存里。
- Actor/组件指针可能因别处代码销毁而突然变成空,使用前要检查。
- 编辑器中对资产执行“强制删除 (Force Delete)”也会把引用它的 UProperty 置空,处理资产对象的代码要能应对。
- 不要对 UObject 使用
TSharedPtr等智能指针,也不要new/delete。
出处
- Unreal Object Handling UE 5.8 文档
- Objects in Unreal Engine UE 5.8 文档
- UnrealBuildTool (UBT) & Targets UE 5.8 文档
- Garbage Collector Internals (Epic KB, Ari Arnbjörnsson, Staff) Epic 员工知识库文章(2022,更新于 2026-09,非版本化文档)
- Incremental Garbage Collection UE 5.7 文档(实验性功能)
- Actor Lifecycle UE 5.8 文档