UE基础—Garbage Collection(垃圾回收)


前言

反射系统

我理解的反射系统:运行时可得到对象的类型信息(包括:属性,方法)、动态创建对象等

UE采用的是将对象的类、属性、方法用关键字(UCLASS,UPROPERTY,UFUNCTIOIN)标记;
编译之前启动UHT去扫描这些关键字,生成/刷新该对象的UClass"伴随对象",UClass保存了反射相关的信息,描述了各个属性之间的内存布局,通过UObject对象实例就可以将相应的属性的值取出来,进行读/写操作。

垃圾回收功能

如果让你来实现这个垃圾回收系统,你觉得垃圾回收系统应该具备哪些功能呢?

  • 实现自定义的NewObject方法(无需调用对应的DeleteObject)
  • 自动回收没有被引用的垃圾对象
  • 不影响其他系统的正常功能

垃圾回收伪实现

垃圾回收的算法有很多, 标记-清除算法 、 复制算法 、标记-整理算法 、分代收集算法等。我们就用标记清除算法来实现。标记-清除算法,看名字就知道有两个阶段,标记和清除:

  • 标记:遍历所有对象,根据某种规则,标记其是否需要清除
  • 清除:遍历所有对象,清除标记了的对象,回收内存

因此可知,要实现标记清除垃圾回收,在标记阶段我们需要做到以下两点:

  • 能拿到所有对象
  • 确定对象清除的规则

在自定义的NewObject方法内,把生成的对象指针放入全局数组GUObjectArray,这样我们就可以拿到所有对象了

想象一个画面:空场景内站着一个英雄。这个情况下,垃圾回收系统是不是应该围绕着英雄来判断?英雄用到的对象就保留,没用到的对象就清除。此时这个英雄就是“根对象”。因此,标记清除的规则就是,根对象用到的对象保留,其他对象清除。那根对象怎么确定?就得我们“手动”标记(AddRoot)

UE垃圾回收实现

UE的垃圾回收过程,正是照着上文的伪实现一步步执行的

UE GC核心算法

UE4垃圾回收的核心是使用了标记-清理算法,标记指的是它以所有在ROOT上的UObject列表为根,去递归遍历所有这些根UObject的引用链,所有能访问到的UObject就标记为可达的。

清理阶段会在标记完成后,收集所有这些UnReachabale UObjects,对其进行清理回收。

标记流程在一帧内完成,而清理阶段可以分为多个帧,时间切片方式完成,所以GC机制的性能点在于标记阶段,如果有太多的UObject对象,那么游戏GC时会有明显的卡顿,所以需要控制内存中UObject数量。

标记-清理算法

纳入被自动GC机制管理的条件:

  • UObject对象中的UObject*成员变量必须被UPROPERTY宏定义修饰,自动被加入到引用。
  • UObject对象实现了派生的AddReferencedObjects接口,手动将UObject*变量添加到引用。
  • 对于Struct结构体中引用的UObject,必须继承FGCObject对象,并实现派生的AddReferencedObjects接口,手动将UObject*变量加入引用。

一、GC的准备:

1.1 初始化全局数组GUObjectArray

  1. 需要获取所有的对象:把生成的对象的指针放入一个全局数组GUObjectArray,每创建一个对象时,为这个对象分配一个InternalIndex,GUObjectArray存放的是结构体FUObjectItem。

FUObjectItem结果如下:

//UObjectArray.h
//对象存储的结构体,GC操作的就是这个对象
struct FUObjectItem
{
   
   class UObjectBase* Object; //对象
   int32 Flags;               //EInternalObjectFlags标识
   int32 ClusterRootIndex;    //当前所属簇索引
   int32 SerialNumber;        //对象序列码(WeakObjectPtr实现用到它)
}
  1. 核心源码解析
    (完整代码参考自 UnrealEngine/Engine/Source/Runtime/CoreUObject/Public/UObject/UObjectArray.h.cpp
  • 对象索引分配 - AllocateUObjectIndex 函数
int32 FUObjectArray::AllocateUObjectIndex(UObjectBase* Object)
{
   
    // 加锁保证线程安全
    FScopeLock Lock(&GUObjectArrayLock);
    
    // 分配新索引
    const int32 Index = ObjObjects.Add(Object);
    Object->InternalIndex = Index;
    
    // 维护对象列表
    ObjAvailableList.RemoveAt(Index);
    ObjObjects[Index] = Object;
    
    return Index;
}
  • UObjectBase 构造函数 - 自动注册到全局数组
UObjectBase::UObjectBase(UObject* InOuter, FName InName, EObjectFlags InFlags)
{
   
    // 分配对象索引(核心注册逻辑)
    InternalIndex = GUObjectArray.AllocateUObjectIndex(this);
    
    // 初始化其他属性
    Outer = InOuter;
    Name = InName;
    Flags = InFlags;
    
    // 将对象加入全局表
    GUObjectArray.IndexToObject(InternalIndex)->SetFlags(InFlags);
}

1.2 获取Obj对象所引用的其他UObject对象

获取Obj对象所引用的其他UObject对象(用于标记时执行),伪代码如下:

//遍历Obj对象的所有属性,如果属性的类型是UObjectle类型,就取消不可达标签
for(TFieldIterator<FProperty> PropertyIter(Obj->GetClass()); 
    PropertyIter; ++PropertyIter)
{
   
    const FProperty* PropertyIns = *PropertyIter;
    //该属性是否是UObject,是否是TArray<UObject>等
    //然后依次取消 "不可达"标记
    //然后递归遍历该属性对象所引用的对象
}

这么实现,确实可以;但是UE并没有这么做,为什么呢?因为属性内其实大部分非UObject类型(比如:int32,bool等),全部遍历效率太低。因此在生成UObject对应UClass对象的时候,就构造了一个新的概念,将所引用的其他UObject对象用一个很巧妙的整数,存入ReferenceTokenStream遍历的Tokens 数组内。

UE会根据反射的信息,将对象的引用信息保存在对应UClass内的ReferenceTokenStream内;
ReferenceTokenStream结构内只有2个数组,起作用的是Tokens (TokenDebugInfo 是调试信息),Tokens 内的数字的结构如下:

struct FGCReferenceInfo
{
   
    union
    {
   
        struct
        {
   
            uint32 ReturnCount  : 8;
            uint32 Type         : 5;
            uint32 Offset       : 19;
        };
        //ReturnCount + Type  + Offset
        //00000000    + 00000 + 0000000000000000000
        uint32 Value; 
    };
};  

1.2.1 ReferenceTokenStream 的作用

ReferenceTokenStream 是 UE 用于记录对象引用关系的二进制数据流,每个 UObject 在序列化(Serialize)时会生成该数据流。其本质是一个紧凑的二进制指令集,记录了:

  • 引用的 UObject 属性偏移量
  • 数组/容器中引用的 UObject
  • 弱引用(Weak Reference)的特殊标记
  • 引用类型(Persistent/Non-Persistent)

1.2.2 生成时机

引用信息在 对象首次加载或动态创建时 通过 UObject::Serialize 函数生成:

// Engine/Source/Runtime/CoreUObject/Private/UObject/Class.cpp
void UObject::Serialize(FArchive& Ar)
{
   
    // 序列化时生成引用信息
    GetClass()->SerializeTaggedProperties(Ar, (uint8*)this, GetClass(), nullptr);
}

二、GC流程

2.1 GC启动

  • 手动调用:UWorld::ForceGarbageCollection( bool bFullPurge),他会在World.tick的下一帧强行进行垃圾回收。
  • 自动调用:系统会根据默认的设置(可重新配置)一定的间隔时间或者条件下,自动调用垃圾回收。

2.2 GC入口

入口阶段:

void CollectGarbage(EObjectFlags KeepFlags, bool bPerformFullPurge)
{
   
	if (GIsInitialLoad)
	{
   
		// During initial load classes may not yet have their GC token streams assembled
		UE_LOG(LogGarbage, Log, TEXT("Skipping CollectGarbage() call during initial load. It's not safe."));
		return;
	}

	// No other thread may be performing UObject operations while we're running
	AcquireGCLock();

	// Perform actual garbage collection
	UE::GC::CollectGarbageInternal(KeepFlags, bPerformFullPurge);

	if (GGCLockBehavior == FGCLockBehavior::Legacy)
	{
   
		// Release the GC lock to allow async loading and other threads to perform UObject operations under the FGCScopeGuard.
		ReleaseGCLock();
	}
	
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值