UE垃圾回收

UE源码解析系列

本文参考UE5.0.3源码

UE GC

UObject管理

UObject是UE的基石,其内部继承体系如下:

1
2
3
4
// UE_5.0\Engine\Source\Runtime\CoreUObject文件夹
class COREUOBJECT_API UObjectBase
class COREUOBJECT_API UObjectBaseUtility : public UObjectBase
class COREUOBJECT_API UObject : public UObjectBaseUtility

其中,UObjectBase提供底层实现,UObjectBaseUtility提供功能函数,都不建议在游戏代码中直接使用。

COREUOBJECT_API是定义为DLLEXPORT的宏,后者用于导出函数到DLL,在MSVC下定义为__declspec(dllexport),在GCC和Clang下定义为__attribute__((visibility("default")))

UObjectArray.h中声明了用于全局UObject管理的GUObjectArrayGUObjectClusters

1
2
extern COREUOBJECT_API FUObjectArray GUObjectArray; // 变量定义在UObjectHash.cpp
extern COREUOBJECT_API FUObjectClusterContainer GUObjectClusters; // 变量定义在UObjectArray.cpp

FUObjectArray内部持有FChunkedFixedUObjectArray对象,后者内部持有FUObjectItem二级指针,管理划分为固定大小(\(2^{16}\))的指针块。FUObjectItem内部持有UObjectBase指针:

1
2
3
4
5
6
7
8
// 仅列出部分数据成员
struct FUObjectItem
{
class UObjectBase* Object; // 持有EObjectFlags
int32 Flags; // 即EInternalObjectFlags
int32 ClusterRootIndex;
int32 SerialNumber; // 与此对象关联的序列号(弱对象指针)
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 对象flag
enum EObjectFlags
{
RF_NoFlags = 0x00000000,

// 本组flag决定对象类型,除了transient都是持久化flag,GC倾向于关注这些
RF_Public =0x00000001, // 其包外可见的对象
RF_Standalone =0x00000002, // 保留对象用于编辑,即使没有被引用
RF_MarkAsNative =0x00000004, // 对象(UField)将在构造时被标记为native(不要在HasAnyFlags()等处使用)
RF_Transactional =0x00000008, // 对象是事务性的
RF_ClassDefaultObject =0x00000010, // 对象是CDO
RF_ArchetypeObject =0x00000020, // 此对象是其它对象的模板,和CDO类似
RF_Transient =0x00000040, // 不保存对象

// 本组flag主要与GC有关
RF_MarkAsRootSet =0x00000080, // 对象将在构造时标记为根集,即使未引用也不会被GC(不要在HasAnyFlags()等处使用)
RF_TagGarbageTemp =0x00000100, // 需要使用GC的各种应用程序的临时用户标志,垃圾收集器本身不会解释它

// 本组flag跟踪UObject的生命周期阶段
RF_NeedInitialization =0x00000200, // 未完成初始化,当~FObjectInitializer完成时清除
RF_NeedLoad =0x00000400, // 加载中,指示对象需要加载
RF_KeepForCooker =0x00000800, // 在GC中保留此对象,因为它正被cooker使用
RF_NeedPostLoad =0x00001000, // 对象需要是加载后的
RF_NeedPostLoadSubobjects =0x00002000, // 加载中,指示对象仍然需要实例化子对象并修正序列化组件引用
RF_NewerVersionExists =0x00004000, // 对象由于其所有者包被重新加载而被丢弃,并且当前存在一个更新的版本
RF_BeginDestroyed =0x00008000, // 对象的BeginDestroy已被调用
RF_FinishDestroyed =0x00010000, // 对象的FinishDestroy已被调用

// 其它flag
RF_BeingRegenerated =0x00020000, // 标记用于创建UClass的UObject(比如蓝图),当它们在加载中重新生成它们的UClass时 (参见FLinkerLoad::CreateExport()),以及被创建中的UClass对象
RF_DefaultSubObject =0x00040000, // 标记默认子对象
RF_WasLoaded =0x00080000, // 标记已经加载的UObject
RF_TextExportTransient =0x00100000, // 不将对象导出为文本格式,通常用于可以从他们的父对象的数据重生成的子对象
RF_LoadCompleted =0x00200000, // 对象已经被linkerload完全序列化过不止一次,不要使用,应用RF_WasLoaded代替
RF_InheritableComponentTemplate = 0x00400000, // 对象的原型可以在它的超类中,补充说明:标记可以被子蓝图继承的组件模板
RF_DuplicateTransient =0x00800000, // 不应该被用于任何类型的复制的对象(复制、粘贴、二进制复制等)
RF_StrongRefOnFrame =0x01000000, // 来自持久化函数帧的对这个对象的引用被认为是强引用
RF_NonPIEDuplicateTransient =0x02000000, // 不应该用于复制的对象,除非它正在为PIE会话被复制
RF_Dynamic UE_DEPRECATED(5.0, "RF_Dynamic should no longer be used. It is no longer being set by engine code.") =0x04000000,
RF_WillBeLoaded =0x08000000, // 此对象在加载中构造并会很快被加载
RF_HasExternalPackage =0x10000000, // 此对象分配有一个外部包,应该在获取最外层的包时查找它

// RF_Garbage和RF_PendingKill在EInternalObjectFlags中被镜像,这样GC检查flag时更快

RF_PendingKill UE_DEPRECATED(5.0, "RF_PendingKill should not be used directly. Make sure references to objects are released using one of the existing engine callbacks or use weak object pointers.") = 0x20000000,
RF_Garbage UE_DEPRECATED(5.0, "RF_Garbage should not be used directly. Use MarkAsGarbage and ClearGarbage instead.") =0x40000000,

RF_AllocatedInSharedPage =0x80000000, // 标记分配在共享内存中的对象
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 用于GC的对象flag
enum class EInternalObjectFlags : int32
{
None = 0,
LoaderImport = 1 << 20, // 对象准备好在加载期间被另一个包导入
Garbage = 1 << 21, // 从逻辑角度来看是垃圾,不应该被引用,出于性能考虑该标志在EObjectFlags中镜像为RF_Garbage
PersistentGarbage = 1 << 22, // 和上面一样,但通过持久化引用被引用,所以无法被GC
ReachableInCluster = 1 << 23, // 簇中存在对象的外部引用
ClusterRoot = 1 << 24, // 簇的根
Native = 1 << 25, // 仅是UClass对象
Async = 1 << 26, // 对象只存在于与游戏线程不同的线程中
AsyncLoading = 1 << 27, // 对象正在被异步加载
Unreachable = 1 << 28, // 对象在对象图上不可达

PendingKill UE_DEPRECATED(5.0, "PendingKill flag should no longer be used. Use Garbage flag instead.") = 1 << 29, // 正在等待析构的对象(游戏玩法中无效的有效对象),出于性能考虑该标志在EObjectFlags中镜像为RF_PendingKill

RootSet = 1 << 30, // 即使没有被引用也不会被GC
PendingConstruction = 1 << 31, // 对象还没有调用其构造函数(只有UObjectBase的构造函数初始化了最基础的成员)

// 以下flag的对象跳过GC
GarbageCollectionKeepFlags = Native | Async | AsyncLoading | LoaderImport,

// EObjectFlags中的镜像Flag
MirroredFlags = Garbage | PendingKill,

AllFlags = LoaderImport | Garbage | PersistentGarbage | ReachableInCluster | ClusterRoot | Native | Async | AsyncLoading | Unreachable | PendingKill | RootSet | PendingConstruction
};

FUObjectClusterContainer内部持有TArray<FUObjectCluster>对象,FUObjectClusterUObject进行分组管理以便于GC。

1
2
3
4
5
6
7
8
9
10
// 仅列出数据成员
struct FUObjectCluster
{
int32 RootIndex; // 根对象索引
TArray<int32> Objects; // 属于此簇的对象
TArray<int32> ReferencedClusters; // 此簇引用的其它簇
TArray<int32> MutableObjects; // 不能添加到此簇但被此簇引用的对象
TArray<int32> ReferencedByClusters; // 引用此簇的簇,当解散簇时使用
bool bNeedsDissolving; // 可能因为PendingKill引用而需要解散的簇
};

UObjectBaseAddObject函数中先根据EObjectFlags设置好EInternalObjectFlags,然后调用GUObjectArray.AllocateUObjectIndex(this);将自己添加到全局UObject数组并分配索引,最后调用HashObject(this);将自己添加到名称哈希表。

GC调用

UE会在固定的时间间隔下自动调用GC(默认61.1秒),可以在UE_5.0\Engine\Config\BaseEngine.ini中配置GC。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[/Script/Engine.GarbageCollectionSettings]
gc.MaxObjectsNotConsideredByGC=1
gc.SizeOfPermanentObjectPool=0
gc.FlushStreamingOnGC=0
gc.NumRetriesBeforeForcingGC=10
gc.AllowParallelGC=True
; pick a fractional number to keep phase shifting and avoid collisions
gc.TimeBetweenPurgingPendingKillObjects=61.1
gc.MaxObjectsInEditor=25165824
gc.IncrementalBeginDestroyEnabled=True
gc.CreateGCClusters=True
gc.MinGCClusterSize=5
gc.AssetClustreringEnabled=False
gc.ActorClusteringEnabled=False
gc.BlueprintClusteringEnabled=False
gc.UseDisregardForGCOnDedicatedServers=False
gc.MultithreadedDestructionEnabled=True
gc.VerifyGCObjectNames=True
gc.VerifyUObjectsAreNotFGCObjects=False
gc.PendingKillEnabled=True

可以通过手动调用GEngine->ForceGarbageCollection(true)bFullPurgeTriggered设置为True,从而强制引擎在UWorld::TickConditionalCollectGarbage()中调用GC。

GC流程

加锁

目的是阻止其它线程执行UObject操作。GCLock是不可重入的。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 开始GC,尝试加锁
void CollectGarbage(EObjectFlags KeepFlags, bool bPerformFullPurge)
{
AcquireGCLock(); // 加锁,FGCCSyncObject::Get().GCLock();
CollectGarbageInternal(KeepFlags, bPerformFullPurge); // 执行GC
ReleaseGCLock(); // 释放锁,FGCCSyncObject::Get().GCUnlock();
}

// 没有其它线程持有GC锁的时候才开始GC,FGCCSyncObject::Get().TryGCLock()
bool TryCollectGarbage(EObjectFlags KeepFlags, bool bPerformFullPurge)
{
// ...
}

void GCLock():自增原子变量GCWantsToRunCounter,通知其它线程想要执行GC,循环等待AsyncCounter(若有任何阻塞GC的非游戏线程则非0)归0,然后自增GCCounter(若GC在执行则非0),创建内存屏障,将GCWantsToRunCounter置为0。

FPlatformMisc::MemoryBarrier()用来创建内存屏障,确保内存屏障之前的所有读写操作都在内存屏障之前完成,内存屏障之后的所有读写操作都在内存屏障之后开始。这样可以保证内存操作的顺序性,避免因为指令重排序导致的问题。

bool TryGCLock():若是AsyncCounter非0则返回false。

标记与可达性分析

目的是找出所有不可达的对象。

这里分析CollectGarbageInternal函数中调用的PerformReachabilityAnalysis函数,其有两个主要步骤,标记与可达性分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 精简代码
void PerformReachabilityAnalysis(EObjectFlags KeepFlags, const EFastReferenceCollectorOptions InOptions)
{
// 获取UObject指针数组ObjectsToSerialize
FGCArrayStruct* ArrayStruct = FGCArrayPool::Get().GetArrayStructFromPool();
TArray<UObject*>& ObjectsToSerialize = ArrayStruct->ObjectsToSerialize;

// 重设对象计数
GObjectCountDuringLastMarkPhase.Reset();

// 确保检查GC referencer对象对其他对象的引用,即使它在永久对象池中
if (FPlatformProperties::RequiresCookedData() && FGCObject::GGCObjectReferencer && GUObjectArray.IsDisregardForGC(FGCObject::GGCObjectReferencer))
{
// 加入FGCObject::GGCObjectReferencer
ObjectsToSerialize.Add(FGCObject::GGCObjectReferencer);
}

const EFastReferenceCollectorOptions OptionsForMarkPhase = InOptions & ~EFastReferenceCollectorOptions::WithPendingKill;

// 1.标记
(this->*MarkObjectsFunctions[GetGCFunctionIndex(OptionsForMarkPhase)])(ObjectsToSerialize, KeepFlags);

// 2.可达性分析
(this->*ReachabilityAnalysisFunctions[GetGCFunctionIndex(InOptions)])(ArrayStruct);

FGCArrayPool::Get().ReturnToPool(ArrayStruct);
}

上述代码中的MarkObjectsFunctions数组(长度为4)保存了MarkObjectsAsUnreachable函数模板的不同实例化,ReachabilityAnalysisFunctions数组(长度为8)保存了PerformReachabilityAnalysisOnObjectsInternal函数模板的不同实例化,实例化参数都是非类型参数EFastReferenceCollectorOptions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
enum class EFastReferenceCollectorOptions : uint32
{
None = 0,
Parallel = 1 << 0,
AutogenerateTokenStream = 1 << 1,
ProcessNoOpTokens = 1 << 2,
WithClusters = 1 << 3,
ProcessWeakReferences = 1 << 4,
WithPendingKill = 1 << 5,
};

// 实例化举例
MarkObjectsFunctions[GetGCFunctionIndex(EFastReferenceCollectorOptions::None)] = &FRealtimeGC::MarkObjectsAsUnreachable<false, false>;

ReachabilityAnalysisFunctions[GetGCFunctionIndex(EFastReferenceCollectorOptions::None)] = &FRealtimeGC::PerformReachabilityAnalysisOnObjectsInternal<EFastReferenceCollectorOptions::None | EFastReferenceCollectorOptions::None>;

// 1.标记
// 将所有没有KeepFlags和EInternalObjectFlags::GarbageCollectionKeepFlags的对象标记为不可达的
template <bool bParallel, bool bWithClusters>
void MarkObjectsAsUnreachable(TArray<UObject*>& ObjectsToSerialize, const EObjectFlags KeepFlags)
{
// ...
}

// 2.可达性分析
// UObject对象对应的UClass对象持有的FGCReferenceTokenStream ReferenceTokenStream记录了对象引用的其它对象,可以据此去除可达对象的不可达标记
// FGCReferenceTokenStream持有TArray<uint32> Tokens
// 一个Token的结构如下:
struct FGCReferenceInfo
{
union
{
struct
{
uint32 ReturnCount : 8; // 当前引用的对象的嵌套深度
uint32 Type : 5; // 引用的类型,比如GCRT_ArrayObject
uint32 Offset : 19; // 当前引用的对象相对于自己的偏移
};
uint32 Value;
};
};
template <EFastReferenceCollectorOptions CollectorOptions>
void PerformReachabilityAnalysisOnObjectsInternal(FGCArrayStruct* ArrayStruct)
{
// ...
}

清理

目的是清理不可达的对象。

CollectGarbageInternal函数中调用IncrementalPurgeGarbage函数进行清理。后者调用的UnhashUnreachableObjects函数调用了UObject::ConditionalBeginDestroy()IncrementalDestroyGarbage函数调用了UObject::ConditionalFinishDestroy()。它们都没有真正调用析构函数,析构函数在TickDestroyObjectsTickDestroyGameThreadObjects中调用。

Conclusion

基于引用计数的GC是实时的,但无法解决使用不当造成的循环引用。基于可达性的GC是非实时的,可以解决循环引用问题。

C++11的智能指针和微软的COM对象使用基于引用计数的GC。

UE使用基于可达性的GC,为标记-清除式。

Java虚拟机使用基于可达性的GC,采用分代机制(GC频率不同),新生代采用标记-复制式,老年代采用标记-整理式。

CPython使用基于引用计数的GC,辅助使用循环引用检测算法和分代机制。

Lua使用基于可达性的GC,为增量标记-清除式。

参考资料

原创 UE基础—Garbage Collection(垃圾回收) - 知乎 (zhihu.com)

Java性能优化之JVM GC(垃圾回收机制) - 知乎 (zhihu.com)

GC 机制探究之 Python 篇 - 知乎 (zhihu.com)

Lua 垃圾回收 菜鸟教程 (runoob.com)

云风的 BLOG: Lua GC 的工作原理 (codingnow.com)


UE垃圾回收
https://reddish.fun/posts/Article/UE-GC/
作者
bit704
发布于
2024年4月23日
许可协议