淺析UE4垃圾回收


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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM