zoukankan      html  css  js  c++  java
  • 浅析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上

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

    标记阶段(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个数

    参考

    虚幻4垃圾回收剖析

  • 相关阅读:
    Redis学习笔记
    RedisTemplate操作命令
    RedisTemplate操作命令
    RedisTemplate操作命令
    RedisTemplate操作命令
    将chrome储存的密码转为MarkDown表格
    使用redisson做redis分布式锁
    RocketMQ 整合SpringBoot发送事务消息
    关于java读写锁的测试
    java8 stream记录
  • 原文地址:https://www.cnblogs.com/kekec/p/13045042.html
Copyright © 2011-2022 走看看