中EN
概念页

垃圾回收与对象生命周期

Garbage Collection & Object Lifecycle

更新于 2026-10-11 约 11 分钟UE5垃圾回收生命周期内存管理GC

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。
  1. Unreal Object Handling UE 5.8 文档
  2. Objects in Unreal Engine UE 5.8 文档
  3. UnrealBuildTool (UBT) & Targets UE 5.8 文档
  4. Garbage Collector Internals (Epic KB, Ari Arnbjörnsson, Staff) Epic 员工知识库文章(2022,更新于 2026-09,非版本化文档)
  5. Incremental Garbage Collection UE 5.7 文档(实验性功能)
  6. Actor Lifecycle UE 5.8 文档

相关页面

游戏开发 · UE5UObject 与反射系统UObject & Reflection System虚幻引擎 (Unreal Engine) 的对象基类是 UObject。在 C++ 类、属性、函数上加 UCLASS/UPROPERTY/UFUNCTION 等宏,再由构建前的预处理工具虚幻头文件工具 (Unreal Header Tool, UHT) 扫描头文件生成代码,引擎就能在运行时“看见”这些类型——这就是反射系统 (Reflection System)。垃圾回收、序列化、编辑器面板、蓝图、网络复制都建立在它之上。游戏开发 · UE5Gameplay 框架Gameplay FrameworkGameplay 框架 (Gameplay Framework) 是 UE 提供的一组相互配合的类,用来搭建游戏的基本结构:Actor 与组件 (Component) 构成场景中的一切;GameMode 在服务器上定规则并生成玩家;玩家由“控制器 (Controller) + 棋子 (Pawn)”组成;GameState 与 PlayerState 负责把全局与每个玩家的状态复制给所有客户端;GameInstance 跨关卡存活。游戏开发 · UE5委托与 Tick 机制Delegates & TickUE 有两种驱动游戏逻辑的方式:事件驱动——委托 (Delegate) 在某事发生时回调绑定的函数;轮询驱动——Tick 每帧(或按最小间隔)调用 Actor/组件的更新函数,并按 Tick 组 (Tick Group) 与物理模拟排序。官方建议能用定时器、委托就不用 Tick,必须 Tick 时用 Tick 组和 Tick 依赖保证顺序,避免“差一帧”问题。游戏开发多线程渲染与任务系统Multithreaded Rendering & Task SystemsCPU 时钟频率约在 2003 年停在 3.4 GHz 附近后,性能来自多核。应用阶段的优化一是让代码与内存访问更缓存友好,二是利用多核:多处理器流水线(时间并行,提高吞吐但增加延迟)、并行处理(空间并行,延迟更低)以及两者结合的任务系统;图形 API 用命令缓冲让多个线程并行录制绘制命令,新一代 API(DirectX 12、Vulkan、Metal)把更多责任交给应用以降低驱动开销。游戏开发 · UE5资产管理与异步加载Asset Management & Async LoadingUE 资产默认按引用自动加载:硬引用 (Hard Reference) 会让被引用资产随引用者一起加载,不加控制就可能在启动时加载全部内容。软引用 (Soft Reference,TSoftObjectPtr/FSoftObjectPath) 只存路径,配合流式管理器 (Streamable Manager) 异步按需加载;资产管理器 (Asset Manager) 进一步把内容划分为主资产 (Primary Asset) 和次资产 (Secondary Asset),用资产包 (Asset Bundle) 管理加载、卸载、烘焙分块与内存审计。游戏开发 · UE5UE5 性能分析UE5 ProfilingUE5 性能分析的基本流程是:先用 stat unit 判断瓶颈在游戏线程、渲染线程还是 GPU,再用对应的 stat 组、profilegpu/GPU 统计或 Unreal Insights 的 Timing/Memory 等视图向下钻取;渲染特性(RDG、VSM、TSR)各自提供作用域和可视化。测量时要避开会扭曲数据的设置(调试构建、异步计算、可视化模式)。游戏开发 · UE5子系统Subsystems子系统 (Subsystem) 是自动实例化、生命周期绑定在宿主对象上的类:Engine、Editor、GameInstance、World、LocalPlayer 五种。宿主创建时自动创建并 Initialize(),宿主关闭时 Deinitialize(),之后无引用即被 GC。它替代了「继承 GameInstance 塞管理器」的老做法,自带按类型的蓝图节点和 Python 访问,特别适合插件。官方编程页只列四种,World 子系统依据 5.8 Python API 补充。游戏开发 · UE5智能指针与 TObjectPtrSmart Pointers & TObjectPtrUE 有两套对象寿命管理:普通 C++ 对象用引用计数智能指针(TSharedPtr/TSharedRef/TWeakPtr/TUniquePtr),UObject 由垃圾回收管理,不能放进这些智能指针。UE5 新增的 TObjectPtr 用来替代 UPROPERTY 里的 UObject 裸指针:编辑器构建中提供动态解析和访问跟踪,非编辑器构建中与裸指针表现相同。游戏开发对象池Object Pool对象池 (Object Pool) 预先一次性分配一组可复用对象,需要时从池里取一个未使用的对象并初始化,用完标记为未使用,而不是逐个 new/delete。它能避免频繁分配的开销和堆碎片,常用于粒子、子弹、正在播放的声音等。代价是池大小要调校、同时活跃的数量有上限、复用的对象不会被自动清零,在有 GC 的环境里还可能让本该回收的对象一直活着。游戏开发游戏对象引用与查询Game Object References & Queries游戏对象需要唯一 id 来区分、查找和充当通信目标;找到对象后还要能引用它,引用的寿命可以从一次函数调用到许多分钟。GEA 比较三种实现:裸指针最快最简单,但有孤儿对象、悬垂指针、非法指针三类问题,且不能配合内存重定位;智能指针可以检测对象已删除、做引用计数,但很难写对,GEA 个人建议远离它,非用不可就用 C++11 标准库或 Boost 的成熟实现;句柄是全局句柄表的整数索引,加上唯一 id 防止槽位复用后指向错误对象,简单而安全。对象查询则不追求通用数据库,而是为最常用的查询建专门的数据结构(GEA4 卷 II 17.5)。综合游戏中的内存管理Memory Management in Games本页把 wiki 中关于内存的内容串成一条线:内存从两方面影响游戏,一是访问模式(缓存未命中远比计算昂贵),二是分配本身(通用堆分配慢且产生碎片,而主机上没有自动换页的虚拟内存兜底)。对策按生命周期组织:常驻与关卡数据用栈、每帧临时数据用单帧/双缓冲分配器、同尺寸小对象用池或小内存分配器、大小不一的动态对象用堆加碎片整理与句柄;资源按引用计数和分块加载管理,流送系统把世界切成等大的块;UE 则在 UObject 层用垃圾回收管理对象生命周期。最后,没有准确的追踪工具,以上一切都无从验证。各结论的细节与出处见所链接的概念页。游戏开发 · UE5容器与字符串Containers & StringsUE 不用 STL,而用自带的值类型容器:TArray(连续数组)、TMap/TMultiMap(哈希映射)和 TSet(哈希集合),都假定元素可按字节直接搬移。字符串分三种:FName 存索引、比较便宜,FText 用于可本地化的界面文本,FString 可以修改但开销最大。选错类型的代价主要是性能和本地化。

输入关键词开始搜索

滚轮或双指缩放 · 拖动平移 · 双击放大 / 还原