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;
}