首页 > 技术文章 > 浅析UE4垃圾回收

kekec 2020-11-22 00:34 原文

垃圾回收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上

LogReferenceChain: (root) MyObject /Engine/Transient.MyObject_0 is not currently reachable.

2. 直接或间接被根节点Root对象引用(UPROPERTY宏修饰的UObject*成员变量     注:UObject*放在UPROPERTY宏修饰的TArrayTMap中也可以)

LogReferenceChain: (root) (standalone) World /Game/ThirdPersonCPP/Maps/ThirdPersonExampleMap.ThirdPersonExampleMap->PersistentLevel
LogReferenceChain:  Level /Game/ThirdPersonCPP/Maps/ThirdPersonExampleMap.ThirdPersonExampleMap:PersistentLevel::AddReferencedObjects(): PersistentLevel
LogReferenceChain:   ThirdPersonCharacter_C /Game/ThirdPersonCPP/Maps/ThirdPersonExampleMap.ThirdPersonExampleMap:PersistentLevel.ThirdPersonCharacter_2->m_Obj2
LogReferenceChain:    MyObject /Engine/Transient.MyObject_0

3. 直接或间接被存活的FGCObject对象引用(如:staticFGCObject)     注:AddReferencedObject在其上的UObject*对象会被其引用

LogReferenceChain: (root) GCObjectReferencer /Engine/Transient.GCObjectReferencer_0::AddReferencedObjects(): Unknown FGCObject
LogReferenceChain:  MyObject /Engine/Transient.MyObject_0

 

注:以上方法只是防止,并不是指一定不会被GC;通过如下操作,仍然可以让这些UObject对象被GC回收掉

① 对于根节点Root上的UObject要调用RemoveFromRoot函数来去除EInternalObjectFlags::RootSet标记

② 没有EInternalObjectFlags::RootSet标记后,就可调用MarkPendingKill函数将把UObject设置为等待回收的对象

 

标记阶段(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对象或USTRUCT结构体对象(该对象自身也要加入到引用链网络中)的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*的TArray成员变量,也会将TArray中对应的UObject*对象清空为nullptr,但不会将该成员从TArray中删除,因此TArrayNum()是不变的

对于UObject*的TMap成员变量,也仅仅是将TMap中对应的key、value的UObject*对象清空为nullptr,不会将该成员从TMap中删除,因此TMapNum()是不变的。另外,可能会导致TMap中有多个key为nullptr的元素,失去了key的唯一性。

 

UObject*成员变量设置成nullptr是在TaskGraph线程上做的

具体置nullptr的代码如下:

 

此时,游戏线程处于等待状态,其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对象的内存。此时如果存在一个野指针(dangling pointer,空悬指针)指向该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锁是否已经被获取了

 

{
   FGCScopeGuard GCGuard; // 进入作用域获取GC锁,离开自动释放GC锁  非GameThread有效
   Package = new FAsyncPackage(*this, *InRequest, EDLBootNotificationManager);
}

 

引擎中的GC逻辑

在Tick中调用GC逻辑

具体实现在:void UEngine::ConditionalCollectGarbage()函数中

 

在LoadMap中以阻塞的方式进行一次全量的GC

具体实现在:void UEngine::TrimMemory()函数中

 

GC相关的设置

这些值的默认设置定义在Engine\Config\BaseEngine.ini中,项目修改这些值后,会保存在项目Config\DefaultEngine.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  ; 地图内执行一次gc mark的间隔时间
; 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  ;分帧执行UObject的BeginDestroy
; 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
gc.AssetClustreringEnabled=False  ; Whether to allow asset files to create actor clusters for GC.

; If false, DisregardForGC(跳过那些不用GC的对象) will be disabled for dedicated servers.
gc.UseDisregardForGCOnDedicatedServers=False  ; Use DisregardForGC On Dedicated Servers

 

注:启动提示Object个数为6586750,超过了SizeOfPermanentObjectPool的配置值6321624,可提高SizeOfPermanentObjectPool数值,来减少GC扫描的Object数目

       LogUObjectAllocator: Warning: |UObjectAllocator.cpp:36|6586750 Exceeds size of permanent object pool 6321624, please tune SizeOfPermanentObjectPool.

 

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  // 多线程析构UObject和释放其内存

; 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

Engine\Config\Android\AndroidEngine.ini中[/Script/Engine.GarbageCollectionSettings]标签下,用gc.MaxObjectsInGame=3000000来指定Android版游戏中允许的最大Object个数

Engine\Config\IOS\IOSEngine.ini[/Script/Engine.GarbageCollectionSettings]标签下,用gc.MaxObjectsInGame=3000000来指定IOS版游戏中允许的最大Object个数

 

参考

虚幻4垃圾回收剖析

 

推荐阅读