当前位置: 首页>后端>正文

19 UE5 UObject的回收和GC介绍

UE Garbage Collection总览介绍

  • UE5中的垃圾回收(Garbage Collection)是自动管理内存的一种机制。它负责识别不再被引用的对象,并释放它们所占用的内存空间。
  • 标记(Marking):垃圾回收从根对象开始,通过可达性分析算法,标记所有仍然被引用的对象。根对象包括活动的游戏对象、全局变量和引擎内部的对象等。
  • 清除(Sweeping):在标记阶段之后,垃圾回收器会遍历整个堆,将未被标记的对象进行清除。清除的对象将释放其所占用的内存空间,并标记为可重用。
  • 压缩(Compacting):在清除阶段之后,垃圾回收会对内存进行压缩。它会将存活的对象向堆的一端移动,以便在堆的另一端形成连续的内存块。这样可以减少内存碎片化,并提高内存的利用率。
  • UE5的垃圾回收是基于分代(Generational)的策略。它将对象分为不同的代(根据对象的存活时间),例如,新创建的对象称为新生代(Young Generation),它们的回收频率较高;而存活时间较长的对象则被称为老生代(Old Generation),它们的回收频率较低。这种分代的策略可以提高垃圾回收的效率。

伪代码

  • 启动垃圾回收,加锁( 保持所有对象的引用关系不变 )
  • 设置所有对象为”不可达”标记(根对象、特殊对象 除外)
  • 遍历根对象列表,根对象引用到的对象去除”不可达”标记
  • 收集所有仍然标记为”不可达”的对象,全部删除

UE5 步骤:

  • 1 GC启动加锁:调用ForceGarbageCollection 或者 系统自动调用。暂停其他线程避免对象引用关系变化,以及执行内存屏障(避免CPU中cache数据不一致问题,可了解 缓存一致性协议)
  • 2 获取所有对象:NewObject的时候,创建的指针会组装为FUObjectItem加入到GUObjectArray中。
//UObjectBase.cpp
//NewUObject方法调用后,UObject对象初始化
UObjectBase::UObjectBase(UClass* InClass..等参数)
{
   AddObject(InName, InInternalFlags); 
}

void UObjectBase::AddObject(FName InName, 
EInternalObjectFlags InSetInternalFlags)
{ 
   //加入GUObjectArray,为Object分配InternalIndex
   GUObjectArray.AllocateUObjectIndex(Object);
}
  • 组装为FUObjectItem加入到GUObjectArray
  • EInternalObjectFlags 可查看枚举类型:是否可到达,是否C++类对象等
//UObjectArray.h
//对象存储结构体,GC操作对象
struct FUObjectItem
{
   class UObjectBase* Object; //对象
   int32 Flags;               //EInternalObjectFlags标识
   int32 ClusterRootIndex;    //当前所属簇索引
   int32 SerialNumber;        //对象序列码(WeakObjectPtr实现用到它)
}
  • 3 引用关系分析和标记:可达对象都放入 ObjectsToSerialize 数组内
  • UObject 引用的其他对象用一个整数,存入ReferenceTokenStream的Tokens数组内
// 将对象标记为不可达,并且将根节点以及不可删除对象放入ObjectsToSerialize
MarkObjectsAsUnreachable(ObjectsToSerialize, KeepFlags);
  • 4 清理:遍历 GObjectArray内的数组仍然被标记为不可达的对象放入 GUnreachableObjects ,随后就是执行清除
  • 主要步骤:
  • a UnhashUnreachableObjects:调用不可达对象的ConditionalBeginDestroy()方法,最终会调用 BeginDestroy()
  • b IncrementalDestroyGarbage:调用不可达对象的ConditionalFinishDestroy()方法,最终会调用 FinishDestroy()
//标记RF_BeginDestroyed
bool UnhashUnreachableObjects(bool bUseTimeLimit, float TimeLimit)
{
   while (GUnrechableObjectIndex < GUnreachableObjects.Num())
   {
      ObjectItem = GUnreachableObjects[GUnrechableObjectIndex++];
      UObject* Object = static_cast<UObject*>(ObjectItem->Object);
      Object->ConditionalBeginDestroy();//调用BeginDestroy();
      ++GUnrechableObjectIndex;
   }
   return bTimeLimitReached;
}

//标记RF_FinishDestroyed
bool IncrementalDestroyGarbage(bool bUseTimeLimit, float TimeLimit)
{
    GObjCurrentPurgeObjectIndex = 0;
   while (GObjCurrentPurgeObjectIndex < GUnreachableObjects.Num())
   {
      FUObjectItem* ObjectItem = GUnreachableObjects[GObjCurrentPurgeObjectIndex];
      UObject* Object = static_cast<UObject*>(ObjectItem->Object);
      Object->ConditionalFinishDestroy();//调用FinishDestroy();
      ++GObjCurrentPurgeObjectIndex;
   }
   // 调用析构函数释放内存
   TickDestroyGameThreadObjects(bUseTimeLimit, TimeLimit, StartTime);
   return bCompleted;
}
  • BeginDestroy() 是通知UObject对象即将被销毁,销毁之前doshting;
  • FinishDestroy() 并没有真正销毁,真正的析构在TickDestroyGameThreadObjects 如下:
FUObjectItem* ObjectItem = GUnreachableObjects[ObjCurrentPurgeObjectIndexOnGameThread];       
if (ObjectItem)
{
   GUnreachableObjects[ObjCurrentPurgeObjectIndexOnGameThread] = nullptr;
   UObject* Object = (UObject*)ObjectItem->Object;
   Object->~UObject();
   GUObjectAllocator.FreeUObject(Object);
}

注意事项

  • 采用 Mark-Sweep 来进行 GC
  • GC 用于 UObject 类型, 对于非 UObject 类型,需要继承 FGCObject 类型
  • UObject 不适用 C++ 的智能指针(但适用于 UE4 的智能指针,譬如 TWeakObjectPtr 等)
  • GC锁:GCLock() 和 GCUnlock()
  • 回收加速优化:Cluster命运共同体(比如:UMaterial,UparticleSystem一簇/群的根)
  • 默认情况下每 60 秒(可修改)执行 TryCollectGarbage 尝试进行一次 GC

UObject 生命周期管理

  • static UMyObject* m_Object 没有引用关系也会自动回收
  • 使用UPROPERTY宏标记UObject,不会被GC
  • 添加UObject到UE4的容器中,并且容器也需要使用UPROPERTY宏标记
  • 如果是在继承UObject类中有一个UObject* A变量,那么即可使用UPROPERTY宏标记一下这个变量,则此变量就不会被UE4自动GC,在该类被销毁的时候,变量A会被设置为null,在之后会被UE4自动GC。
  • 如何保存对象不被析构:AddToRoot
  • 需要释放时:RemoveFromRoot
UMyObject* m_Object = NewObject<UMyObject>();
m_Object->AddToRoot();
m_Object->RemoveFromRoot();

对象指针保持

  • 需要在对象上保持一个指向其他对象的指针,用UPROPERTY()宏
UCLASS()
class AMyClass : public AActor
{
    GENERATED_BODY()
private:
    // 
    UPROPERTY()
    AActor* MyReferenceToAnotherActor;
}

UObject的销毁回调

  • 通过IsValid 和 IsValidLowLevel判断对象是否的有效性
        // GC和回收
        if(1)
        {
            // 异步执行且对象在当前帧内持续有效 等GC触发后失效
            // 会触发调用对象的BeginDestroy()
            pObj->ConditionalBeginDestroy(); 

            // 依然有效
            auto age = pObj->CurPlayerAge; 

            // 强制GC
            GEngine->ForceGarbageCollection(); // 下一帧将强制执行GC
            GEngine->DelayGarbageCollection(); // 下一帧一定不执行GC

            // 还未失效
            if(IsValid(pObj))
            {
                bool bTrueValid = pObj->IsValidLowLevel();
                FString _name = pObj->CurPlayerName;
                auto _age = pObj->CurPlayerAge; 
            }
        }
  • 重载对象的BeginDestroy函数(ConditionalBeginDestroy会进入)
  • 务必记住调用父类的销毁函数(不然引擎会崩溃)
void UTestObject::BeginDestroy()
{
    bool bTrueValid = this->IsValidLowLevel();
    FString _name = CurPlayerName;
    auto _age = CurPlayerAge; 

    // 调用父类销毁
    Super::BeginDestroy();
}

通过StaticOBJ来测试

  • AddToRoot就不会触发GC回收
  • 没有AddToRoot,第二次进测试函数时IsValidLowLevel已然失效
    static UTestObject* StaticObj = NewObject<UTestObject>();
    if(IsValid(StaticObj))
    {
        // 不添加到root 本函数结束时StaticObj会被销毁。
        StaticObj->AddToRoot();

        StaticObj->CurPlayerName = TEXT("S_Mike");
        StaticObj->CurPlayerAge = 38;

        // 首次为T 第二次进入为F (AddToRoot后一直为T)
        bool bTrueValid = StaticObj->IsValidLowLevel();
        FString _name = StaticObj->CurPlayerName;
        auto _age = StaticObj->CurPlayerAge; 
    }

https://www.xamrdz.com/backend/3v61941659.html

相关文章: