垃圾回收(Garbage Collection)算法分类:
分类一 | 引用计数式 |
通过额外的计数来实时计算对单个对象的引用次数,当引用次数为0时回收对象。 如:微软COM对象、句柄的加减引用值以及C++中的智能指针都是通过引用计数来实现GC的 |
追踪式(UE4) | 达到GC条件时(内存不够用、到达GC间隔时间或者强制GC)通过扫描系统中是否有对象的引用来判断对象是否存活,然后回收无用对象 | |
分类二 | 保守式 |
不能准备识别每一个无用的对象(比如在32位程序中的一个4字节的值,它是不能判断出它是一个对象指针或者是一个数字的),但是能保证在不会错误的回收存活的对象的情况下回收一部分无用对象。 不需要额外的数据来支持查找对象的引用,它将所有的内存数据假定为指针,通过一些条件来判定这个指针是否是一个合法的对象 |
精确式(UE4) | 在回收过程中能准确得识别和回收每一个无用对象的GC方式,为了准确识别每一个对象的引用,通过需要一些额外的数据(比如虚幻中的属性UPROPERTY) | |
分类三 | 搬迁式 | GC过程中需要移动对象在内存中的位置,当然移动对象位置后需要将所有引用到这个对象的地方更新到新位置(有的通过句柄来实现、而有的可能需要修改所有引用内存的指针)。 |
非搬迁式(UE4) | 在GC过程中不需要移动对象的内存位置 | |
分类四 | 实时 | 不需要停止用户执行的GC方式 |
非实时(UE4) | 需要停止用户程序的执行(stop the world) | |
分类五 | 渐进式 | 不会在对象抛弃时立即回收占用的内存资源,而在GC达成一定条件时进行回收操作 |
非渐进式(UE4) | 在对象抛弃时立即回收占用的内存资源 |
UE4采用“追踪式、精确式、非搬迁式、非实时、非渐进式”的标记清扫(Mark-Sweep)GC算法。该算法分为两个阶段:标记阶段(GC Mark)和清扫阶段(GC Sweep) 注:以下代码基于UE 4.25.1版本
UObject对象采用垃圾回收机制,被UPROPERTY宏修饰或在AddReferencedObjects函数被手动添加引用的UObject*成员变量,才能被GC识别和追踪,GC通过这个机制,建立起引用链(Reference Chain)网络。
没有被UPROPERTY宏修饰或在AddReferencedObjects函数被没添加引用的UObject*成员变量无法被虚幻引擎识别,这些对象不会进入引用链网络,不会影响GC系统工作(如:自动清空为nullptr或阻止垃圾回收)。
垃圾回收器定时或某些阶段(如:LoadMap、内存较低等)从根节点Root对象开始搜索,从而追踪所有被引用的对象。
当UObject对象没有直接或间接被根节点Root对象引用或被设置为PendingKill状态,就被GC标记成垃圾,并最终被GC回收。
注1:USTRUCT宏修饰的结构体对象和普通的C++对象一样,是不被GC管理
注2:FGCObject对象和普通的C++对象一样,是不被GC管理
基础概念及操作
置nullptr
若将UObject对象的UPROPERTY宏修饰的UObject*成员变量置成nullptr,只会断掉这个节点的子链路
获取FUObjectItem
/** * Single item in the UObject array. */ struct FUObjectItem { // Pointer to the allocated object class UObjectBase* Object; // Internal flags int32 Flags; // UObject Owner Cluster Index int32 ClusterRootIndex; // Weak Object Pointer Serial number associated with the object int32 SerialNumber; }; // 获取UObject对象对应的FUObjectItem FUObjectItem* ObjItem = GUObjectArray.IndexToObject(Obj->GetUniqueID());
Root
1. AddToRoot函数会将UObject对象加到根节点Root上,让其不被GC回收
该UObject对象对应GUObjectArray中的FUObjectItem的Flags会加上EInternalObjectFlags::RootSet标记
2. RemoveFromRoot函数会将UObject对象从根节点Root上移除
会去掉该UObject对象对应GUObjectArray中的FUObjectItem的Flags的EInternalObjectFlags::RootSet标记
标记为PendingKill
1. UObject对象不为Root对象,可通过调用MarkPendingKill函数将把该对象设置为等待回收的对象。
将UObject对象对应GUObjectArray中的FUObjectItem的Flags加上EInternalObjectFlags::PendingKill标记
UObject本身内存数据是没有修改的,可对其成员进行读写
2. 可通过IsPendingKill函数来判断一个UObject是否处于PendingKill状态
3. 调用ClearPendingKill函数来清除PendingKill状态
防止被GC的方法
1. 调用AddToRoot函数将UObject对象加到根节点Root上
2. 直接或间接被根节点Root对象引用(UPROPERTY宏修饰的UObject*成员变量 注:UObject*放在UPROPERTY宏修饰的TArray、TMap中也可以)
标记阶段(GC Mark)
从根节点集合开始,标记出所有不可达的对象。该阶段执行时需要保证对象引用链不被修改,因此是阻塞的
一个对象一旦被标记为不可达,就被贴上垃圾的标签,不可能再被复活,通过FindObject函数也不能获取该对象,只能等待被GC回收
该阶段后,不会修改UObject对象内存块中任何数据
标记对象为不可达
等待回收UObjec对象,在经过GC Mark时,会将对象设置上EInternalObjectFlags::Unreachable标记,此时调用IsUnreachable函数才会返回true
需要注意的是,在GC Mark之前,即使等待回收UObjec对象已经是不可达的,但是此时由于未设置EInternalObjectFlags::Unreachable标记,因此调用IsUnreachable函数仍然会返回false
设置EInternalObjectFlags::Unreachable标记是在TaskGraph线程上做的
此时,游戏线程的Stack如下:
自动更新引用
一个UObject成为等待回收的对象时,以下几种情况:
①赋值给其他UObject对象的UPROPERTY宏修饰的UObject*成员变量
②赋值给其他UObject对象的无UPROPERTY宏修饰的UObject*成员变量,但这些成员变量在重写的静态AddReferencedObjects函数中被手动添加引用
// AMyTest1Character重写静态函数AddReferencedObjects // 将无UPROPERTY宏修饰的成员变量m_Obj3手动添加到引用链中 // 该函数在GC Mark和GC Sweep阶段的过程中都会被调用 void AMyTest1Character::AddReferencedObjects(UObject* InThis, FReferenceCollector& Collector) { AMyTest1Character* This = CastChecked<AMyTest1Character>(InThis); Collector.AddReferencedObject(This->m_Obj3); Super::AddReferencedObjects(InThis, Collector); }
③赋值给其他FGCObject对象的无UPROPERTY宏修饰的UObject*成员变量,但这些成员变量在重写的AddReferencedObjects函数中被手动添加引用
// FTestGCObject重写函数AddReferencedObjects // 将无UPROPERTY宏修饰的成员变量m_Obj3手动添加到引用链中 注:非UObject的对象也不允许添加UPROPERTY宏修 // 该函数在GC Mark和GC Sweep阶段的过程中都会被调用 void FTestGCObject::AddReferencedObjects(FReferenceCollector& Collector) // FTestGCObject : public FGCObject { Collector.AddReferencedObject(m_Obj3); // UMyObject* m_Obj3为FTestGCObject的成员变量 }
在GC Mark阶段,会将UObject*成员变量自动清空为nullptr,以防止出现野指针
将UObject*成员变量设置成nullptr是在TaskGraph线程上做的
此时,游戏线程处于等待状态,其Stack如下:
清扫阶段(GC Sweep)
阶段遍历所有对象,将标记为不可达的对象回收。该阶段可通过限制时间来分帧异步进行,避免导致卡顿
在BeginDestroy函数中将UObject对象的Name设置成空 注:UObject对象的Flags通过RF_BeginDestroyed标志,来防止BeginDestroy函数执行多次
在FinishDestroy函数中销毁所有UObject对象的非Native的属性 注:UObject对象的Flags通过RF_FinishDestroyed标志,来防止FinishDestroy函数执行多次
最后,在TickDestroyObjects函数中调用UObject的析构函数,并调用GUObjectAllocator.FreeUObject函数来释放内存
判断UObject对象有效性
IsValid全局函数
判断UObject对象指针是否为空以及是否为PendingKill状态
IsValidLowLevel成员函数
依次检查:①UObject对象指针是否为空 ②UObject对象的Class是否为空 ③检查UObject对象的Index是否有效 ④在全局表GUObjectArray中对应的FUObjectItem中对象是否为空,是否与原UObject对象相同
在进行GC Sweep时,在调用UObject的析构函数中,IsValidLowLevel函数仍然能返回true
只有执行GUObjectArray.FreeUObjectIndex函数,发出NotifyUObjectDeleted通知时,IsValidLowLevel函数才返回false
IsValidLowLevelFast成员函数
依次检查:①UObject对象指针是否为空或小于0x100,是否8字节对齐 ②UObject对象的虚表是否为空 ③UObject对象的ObjectFlags是否有效
④UObject对象的Class、Outer是否8字节对齐 ⑤UObject对象的Class及Class的CDO对象是否为空、Class的CDO对象是否8字节对齐
⑥UObject对象的Index是否在全局表GUObjectArray范围内 ⑦UObject对象的Name是否有效
⑧如果参数bool bRecursive为true,还会对UObject对象的Class执行IsValidLowLevelFast(false)检查
GC Sweep后,GUObjectAllocator.FreeUObject函数会回收掉这个UObject对象的内存。此时如果存在一个野指针指向该UObject,调用IsValidLowLevelFast(true)函数会返回false
注:野指针调用IsValidLowLevelFast函数本身是非法的,是未定义行为
注意:在PIE下执行GC没有效果,PC上需要在Standalone下执行
执行GC操作的函数
以阻塞的方式尝试进行一次GC Mark
GEngine->PerformGarbageCollectionAndCleanupActors();
TryCollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS, false); // ① 会先检查在其他线程中是否有UObject操作 ② 连续尝试没成功的次数 > GNumRetriesBeforeForcingGC时 注:UE4.25中GNumRetriesBeforeForcingGC配置为10
GEngine->ForceGarbageCollection(false); // 下一帧才以阻塞的方式尝试进行一次GC Mark
以阻塞的方式进行一次GC Mark
CollectGarbage(RF_NoFlags, false);
CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS, false);
如果连续2次调用GC Mark,在第2次GC Mark之前,会先阻塞执行一次全量的GC Sweep
限制时间来分帧进行一次GC Sweep
IncrementalPurgeGarbage(true); // 以缺省0.002的时间进行一次GC Sweep
IncrementalPurgeGarbage(true, 0.1); // 以0.1的时间进行一次GC Sweep
引擎在每帧Tick中都在通过限制时间来分帧异步进行GC Sweep
阻塞的方式进行一次GC Sweep
IncrementalPurgeGarbage(false); // 以阻塞的方式进行一次GC Sweep
以阻塞的方式尝试进行一次全量的GC(包括Mark和Sweep阶段)
TryCollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS, true);
GEngine->Exec(nullptr, TEXT(“obj trygc”));
GEngine->ForceGarbageCollection(true); // 下一帧才以阻塞的方式尝试进行一次全量的GC
以阻塞的方式进行一次全量的GC(包括Mark和Sweep阶段)
CollectGarbage(RF_NoFlags);
CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS);
CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS, true);
GEngine->Exec(nullptr, TEXT(“obj gc”));
GC相关的代理
static FSimpleMulticastDelegate& GetPreGarbageCollectDelegate(); // GC Mark或全量GC执行之前的代理通知
static FSimpleMulticastDelegate& GetPostGarbageCollect(); // GC Mark或全量GC完成之后的代理通知
static FSimpleMulticastDelegate PreGarbageCollectConditionalBeginDestroy; // GC Sweep ConditionalBeginDestroy之前的代理通知
static FSimpleMulticastDelegate PostGarbageCollectConditionalBeginDestroy; // GC Sweep ConditionalBeginDestroy完成之后的代理通知
static FSimpleMulticastDelegate PostReachabilityAnalysis; // GC Mark可达性分析之后的代理通知
GC相关的状态API
bool IsGarbageCollectingOnGameThread() // GC是否在游戏线程上
bool IsInGarbageCollectorThread() // 是否在GC线程上
bool IsGarbageCollecting() // 是否正在执行GC逻辑
bool IsGarbageCollectionWaiting() // GC是否在等待运行
GC锁
使得在垃圾回收时,其他线程的任何UObject操作都不会工作,避免出现一边回收一边操作导致的问题
FGCCSyncObject::Get().TryGCLock(); // 尝试获取GC锁
AcquireGCLock(); // 获取GC锁
ReleaseGCLock(); // 释放GC锁
bool IsGarbageCollectionLocked() // GC锁是否已经被获取了
引擎中的GC逻辑
在Tick中调用GC逻辑
具体实现在:void UEngine::ConditionalCollectGarbage()函数中
在LoadMap中以阻塞的方式进行一次全量的GC
具体实现在:void UEngine::TrimMemory()函数中
GC相关的设置
这些值的默认设置定义在EngineConfigBaseEngine.ini中,项目修改这些值后,会保存在项目ConfigDefaultEngine.ini中
[/Script/Engine.GarbageCollectionSettings] ; Placeholder console variable, currently not used in runtime. gc.MaxObjectsNotConsideredByGC=24575 ;NoGC对象长度 用于标记这个数组的前多少个元素要被GC跳过。在初始化时也预先在数组中添加了这么多个空元素 ; Placeholder console variable, currently not used in runtime. gc.SizeOfPermanentObjectPool=6321624 ; If enabled, streaming will be flushed each time garbage collection is triggered. gc.FlushStreamingOnGC=0 ; Maximum number of times GC can be skipped if worker threads are currently modifying UObject state. gc.NumRetriesBeforeForcingGC=10 ; sed to control parallel GC. gc.AllowParallelGC=True ; Time in seconds (game time) we should wait between purging object references to objects that are pending kill. gc.TimeBetweenPurgingPendingKillObjects=60.000000 ; Placeholder console variable, currently not used in runtime. gc.MaxObjectsInEditor=25165824 ; Maximum number of UObjects in the editor ; If true, the engine will destroy objects incrementally using time limit each frame gc.IncrementalBeginDestroyEnabled=True ; If true, the engine will attempt to create clusters of objects for better garbage collection performance. gc.CreateGCClusters=True ; Create Garbage Collector UObject Clusters ; Minimum GC cluster size gc.MinGCClusterSize=5 ; Whether to allow levels to create actor clusters for GC. gc.ActorClusteringEnabled=False gc.BlueprintClusteringEnabled=False ; Blueprint Clustering Enabled ; If false, DisregardForGC will be disabled for dedicated servers. gc.UseDisregardForGCOnDedicatedServers=False ; Use DisregardForGC On Dedicated Servers
GC相关的ConsoleVariable
;Placeholder console variable, currently not used in runtime. gc.MaxObjectsInGame ; int Maximum number of UObjects in cooked game ; Maximum number of UObjects for programs can be low gc.MaxObjectsInProgram ; int Default to 100K for programs ;If true, the UObjectArray will pre-allocate all entries for UObject pointers gc.PreAllocateUObjectArray ; bool ;If true, the engine will free objects' memory from a worker thread gc.MultithreadedDestructionEnabled ; If set to 1, the engine will attempt to trigger GC each frame while async loading. gc.StressTestGC ; If set to 1, the engine will force GC each frame. gc.ForceCollectGarbageEveryFrame ; Used to debug garbage collection...Collects garbage every frame if the value is > 0. gc.CollectGarbageEveryFrame ; Multiplier to apply to time between purging pending kill objects when on an idle server. gc.TimeBetweenPurgingPendingKillObjectsOnIdleServerMultiplier ; Time in seconds (game time) we should wait between purging object references to objects that are pending kill when we're low on memory gc.LowMemory.TimeBetweenPurgingPendingKillObjects ; Time in seconds (game time) we should wait between GC when we're low on memory and there are levels pending unload gc.LowMemory.TimeBetweenPurgingPendingLevels ; Memory threshold for low memory GC mode, in MB gc.LowMemory.MemoryThresholdMB ;Minimum number of objects to spawn a GC sub-task for. gc.MinDesiredObjectsPerSubTask ; Dumps count and size of GC Pools gc.DumpPoolStats ; Dumps all clusters do output log. When 'Hiearchy' argument is specified lists all objects inside clusters. gc.ListClusters ; Dumps all clusters do output log that are not referenced by anything. gc.FindStaleClusters ; Dumps references to all objects within a cluster. Specify the cluster name with Root=Name. gc.DumpRefsToCluster
EngineConfigAndroidAndroidEngine.ini中[/Script/Engine.GarbageCollectionSettings]标签下,用gc.MaxObjectsInGame=3000000来指定Android版游戏中允许的最大Object个数
EngineConfigIOSIOSEngine.ini中[/Script/Engine.GarbageCollectionSettings]标签下,用gc.MaxObjectsInGame=3000000来指定IOS版游戏中允许的最大Object个数
参考