《Unreal 对 C++ 做了什么》系列 (10/54)
10. 垃圾回收器 (GC) 原理:标记-清除与 GC 根对象 🧹
🚀 导言:为什么 C++ 需要垃圾回收?
在传统的 C++ 开发中,“谁申请,谁释放”是金科玉律。但在拥有数万个相互引用对象的游戏引擎中,手动管理引用链几乎是不可能的。如果 Actor A 引用了数据 B,而 B 又引用了特效 C,手动删除 A 时,你必须确保 B 和 C 没有被其他地方引用。
虚幻引擎通过一套名为Mark and Sweep(标记-清除)的垃圾回收机制,解决了这个难题。
🔑 1. 核心算法:标记-清除 (Mark and Sweep)
UE 的 GC 并不是实时的(不像 C# 的分代回收),而是一种追踪式的回收。它分为两个主要阶段:
阶段 A:标记 (Marking)
GC 会从一组被称为Root Set(根集)的对象开始,顺着它们身上的UPROPERTY指针向下摸索。
- 只要是能从“根”顺着指针摸到的对象,就被标记为“可达(Reachable)”。
- 没被摸到的对象,就像是断开连接的孤岛,被标记为“不可达”。
阶段 B:清除 (Sweeping)
引擎遍历全局对象表(GUObjectArray),将所有没有“可达”标记的对象彻底从内存中抹除,并归还内存。
🔑 2. 什么是“根” (Root Set)?
如果所有对象都被 GC,那谁来保护第一批对象不被回收?答案就是Root Set。
以下对象会自动进入根集,成为 GC 扫描的起点:
UGameEngine:引擎对象。UWorld:当前的关卡及其所有的 Actor。UGameInstance:贯穿游戏始终的全局对象。- 被标记为
Root的对象:你可以通过AddToRoot()手动将一个对象固定在内存中(记得用RemoveFromRoot()释放,否则会内存泄漏)。
🔑 3. 为什么UPROPERTY如此重要?
这是初学者最容易崩溃的地方:GC 只认UPROPERTY。
UCLASS()classAMyCharacter:publicACharacter{GENERATED_BODY()// 情况 1:GC 知道 Character 引用了 WeaponUPROPERTY()UWeapon*MyWeapon;// 情况 2:GC 彻底无视这个指针!UWeapon*HiddenWeapon;};- 情况 1:当 GC 扫描 Character 时,发现它有一个
UPROPERTY指向MyWeapon。于是MyWeapon被标记为可达,幸免于难。 - 情况 2:虽然你在 C++ 里赋值了
HiddenWeapon,但 GC 扫描时看不见它。GC 会认为HiddenWeapon指向的对象没有人用,直接将其回收。此时HiddenWeapon就成了一个野指针,一旦访问立即崩溃。
🔑 4. 性能克星:GC 引起的卡顿 (Hitch)
GC 扫描几万个对象是需要时间的。在早期版本中,GC 会“冻结”整个游戏线程来执行扫描,这就是为什么老游戏有时会突然卡一下。
UE 对 C++ 做的优化:
- 并行标记 (Parallel Mark):利用多线程同时扫描不同的对象树。
- 增量清除 (Incremental Reachability Analysis):将扫描工作拆分到多帧完成,避免单帧耗时过长。
- 簇 (Clustering):将一些永远会在一起的对象(如某个 Actor 及其所有 Component)打包成一个“簇”。GC 只需要检查 Actor 是否可达,就可以直接判定整个簇的状态,极大提升速度。
📊 GC 运转流程表
| 步骤 | 执行内容 | 开发者职责 |
|---|---|---|
| 1. 注册 | 对象创建时进入GUObjectArray | 使用NewObject或SpawnActor |
| 2. 追踪 | GC 扫描UPROPERTY引用链 | 务必给 UObject 指针加UPROPERTY() |
| 3. 标记 | 判定对象是否可达 | 正常引用即可,特殊情况使用AddToRoot |
| 4. 销毁 | 释放不可达对象的内存 | 确保没有非UPROPERTY指针指向它 |
⚠️ 避坑指南:如何与 GC 和谐共处?
- 容器保护:如果你用
TArray<UObject*>存对象,这个数组也必须加UPROPERTY(),否则数组里的对象会被当成垃圾清理掉。 - **避免手动
delete**:永远不要对UObject使用delete。如果你想让它消失,取消对它的引用,或者调用MarkAsGarbage()(UE5)。 - 静态变量风险:普通的
static UObject* MyGlobalPtr无法被UPROPERTY标记。如果必须使用全局 UObject,请考虑将其放入UGameInstance中管理。
结语
垃圾回收是虚幻引擎为 C++ 开发者提供的“安全气囊”。它通过UPROPERTY引用链建立了一套自动化的生存法则。只要你遵循“凡是 UObject 指针必加宏”的原则,你就已经掌握了 UE 内存管理的一半真谛。
下一篇预告:《11. 弱引用与软引用:TWeakObjectPtr 和 TSoftObjectPtr》。我们将探讨:如果我不希望一个指针“保住”对象的命,或者我希望延迟加载对象,该怎么办?