UEGC分析
UE标记清除GC算法特点
UE4采用“追踪式、精确式、非搬迁式、非实时、非渐进式”的标记清扫(Mark-Sweep)GC算法。该算法分为两个阶段:标记阶段(GC Mark)和清扫阶段(GC Sweep)
跟踪式:达到GC条件时,通过扫描系统中是否有对象引用的方式来判断是否回收
- 引用计数:额外分配空间用于存储引用计数,引用为0时回收对象本身
精确式:需要额外的数据来判断,以识别每个对象的引用(比如标识符UPROPERTY)
- 保守式:不需要额外的数据来支持对象的判断
非搬迁式:不需要移动GC对象的内存位置
- 搬迁式:例如标记复制法,将成功标记的对象复制到另一个堆中,剩下的对象就可以被GC了
非实时:需要停止用户程序的执行
- 实时:不需要停止用户执行
非渐进式:对象被GC后立即回收占用的内存资源
- 渐进式:在GC达成一定条件的时候进行回收操作
被UPROPERTY宏修饰,或者在AddReferencedObjects手动添加引用的UObject*成员变量,才能被GC识别。
首先说一下UE中GC的大致流程:
- 启动GC时加锁,防止GC时引用关系发生变化
- 将所有对象设置为“不可达”(特殊对象、根对象除外)
- 从根对象开始遍历,引用到的对象去除“不可达”标记
- 将所有“不可达”删除
基础操作
使用:在大多数情况下,继承自UObject类的变量,前面加一个UPROPERTY宏,那么这个变量自然就被纳入UE的垃圾回收系统之中了。
如果变量继承自非UObject类的对象变量,那么这个这个承载这个变量需要继承自FGCObject类,并且该类需要重写实现FGCObject提供的AddReferencedObjects接口,在接口中调用AddReferencedObject方法手动添加该对象至Collector实现。
也就是非UObject引用了UObject类对象
//.h
class FTestClass : public FGCObject
{
public:
FTestClass();
class UTestObject* MyObject;
virtual void AddReferencedObjects(FReferenceCollector& Collector) override;
};
//.cpp
FTestClass::FTestClass()
{
MyObject = NewObject<UTestObject>(GetTransientPackage(), TEXT("TestGCPlgTestObject"));
}
void FTestClass::AddReferencedObjects(FReferenceCollector& Collector)
{
Collector.AddReferencedObject(MyObject);
}
至于其他类型的对象,则使用智能指针或者其他方法进行GC管理
Q:如果一个对象在拥有UPROPERTY标记的同时又被AddReferenceObjects手动添加进GC系统会发生什么?
其他操作方法
AddToRoot()将UObject对象添加到根节点Root上,这样就不会被GC回收了,对应的方法为RemoveFromRoot()。是通过将对象对应GUObjectArray中的FUObjectItem的Flags会加上EInternalObjectFlags::RootSet标记实现的。
IsValid()这个函数检查对象是否有效,对应C++中的“!=null”操作,但是这个函数本身可以对对象进行更多的检查。
- 检查UObject指针指向是否为空——为空则返回false
- 检查对象内部的
InternalFlags
,如果被标记为待销毁,则返回false - 检查对象是否正在被垃圾回收,如果是,则返回false
#include "UObject/UObjectGlobals.h"
bool IsValid(const UObject* Object)
{
if (Object == nullptr)
{
return false;
}
if (Object->IsPendingKill())
{
return false;
}
if (Object->HasAnyFlags(RF_BeginDestroyed) || Object->HasAnyFlags(RF_FinishDestroyed))
{
return false;
}
return true;
}
MarkPendingKill()手动将对象设置为等待回收的对象,意思就是跳过GC一系列的算法,直接在下一轮将对象回收。是通过将对象对应GUObjectArray中的FUObjectItem的Flags加上EInternalObjectFlags::PendingKill标记来实现的。
防止被GC
- 使用AddToRoot函数
- 直接或间接被Root对象引用
- 直接或间接被FGCObject对象引用
主动GC
CollectGarbage()在当前帧内进行一次垃圾回收
ForceCollectGarbage()在下一帧内进行一次垃圾回收
判断UObject对象有效性
IsValid()——判断指针是否为空,判断是否为PendingKill
基础定义
类EInternalObjectFlags
FUObjectItem用于保存UObject信息,存有一个EInernalObjectFlags的枚举变量,这个变量代表着当前对象的情况,比如PendingKill代表对象就要被回收了。
在类型系统中的其他Flags:
- EObjectFlags:对象本身的标志。
- EInternalObjectFlags:对象存储的标志,GC的时候用来检查可达性。
- EObjectMark:用来额外标记对象特征的标志,用在序列化过程中标识状态。
- EClassFlags:类的标志,定义了一个类的特征。
- EClassCastFlags:类之间的转换,可以快速的测试一个类是否可以转换成某种类型。
- EStructFlags:结构的特征标志。
- EFunctionFlags:函数的特征标志。
- EPropertyFlags:属性的特征标志。
来自大钊的《InsideUE4》
ReachableInCluster = 1 << 23, ///< 集群中存在对对象的外部引用
ClusterRoot = 1 << 24, ///< 集群的根 Native = 1 << 25, ///< 本地的(仅UClass)。 Async = 1 << 26, ///< 对象只存在于与游戏线程不同的线程上。 AsyncLoading = 1 << 27, ///< 对象正在异步加载。 Unreachable = 1 << 28, ///< 对象在对象图(Graph)上不可达。 PendingKill = 1 << 29, ///< 等待销毁的对象(在GamePlay中无效,但暂时还依然有效的对象) RootSet = 1 << 30, ///< 对象将不会被垃圾回收,即使未引用。
如何看待FUObjectItem?这个结构在某UObject被纳入UE的GC管理后被分配,为一对一的关系,用于存储该对象的一些GC相关的元数据。也并不一定为一对一的关系,比如某些UObject不被纳入此UE的正常GC之中则不会被分配。
由于FUObjectItem本身只是结构体,而且在GUObjectArray中排列紧密,所以在GC的扫描中能很方便得直接进行For循环遍历,而且此For循环也可以使用多线程版本的For循环。
//UObjectArray.h
struct FUObjectItem
{
class UObjectBase* Object; //对象,使用newObject时返回的对象指针
int32 Flags; //EInternalObjectFlags标识
int32 ClusterRootIndex; //当前所属簇索引
int32 SerialNumber; //对象序列码(WeakObjectPtr实现用到它)
}
GC流程
首先从CollectGarbage()开始,这个函数做了三件事:
- 获取GC锁
- 执行CollectGarbageInternal
- 释放GC锁
UE引擎中的GC是多线程的,所以要设置GC锁。比如防止一个对象加载后,其引用还没被添加,就被当成垃圾被GC掉,所以这个锁的目的是防止其他线程对这个UObject进行相关操作。
重点是CollectGarbageInternal,这个函数之内执行标记和清扫操作。
标记
PerformReachabilityAnalysis()方法:
- 在ObjectsToSerialize中添加FGCObject::GGCObjectReferencer,用于在非UObject对象上调用AddReferencedObjects方法。
- 调用MarkObjectsAsUnreachable,将不带KeepFlags标记的对象标记为不可达。
GUObjectArray,这个全局的变量保存了所有的UObject(由FUObjectItem封装)。可以通过下标来找到UObject,此下标就存储在UObjectBase::InternalIndex属性之中。
此外,这个UObject列表的前部有一些不被纳入GC的obj,会在gc扫描时特地排除。
- PerformReachabilityAnalysisOnObjects判断可达。
ReferenceToken是一组token流,用于描述类中对象的引用情况,使用它的原因是代替UProperty来扫描对象引用关系,达到更高效率。
每一个UObject都有一个对应的UClass来保存这个对象中每一个属性的相关信息,可以通过这个对象的实例化地址来遍历所有的属性,这样也能得到所有的引用对象。但是时则以的效率太慢,因为大部分情况下对象的属性都不是UObject类型。
token流的意思就是将一系列数据转化为uint32值,这个值的实际意义是这个对象引用的其他对象的偏移位置,使之更方便传输与缓存。
token在UE中由FGCReferenceInfo这个类来描述,具体如下:
/** Mapping to exactly one uint32 */ union { /** Mapping to exactly one uint32 */ struct { /** Return depth, e.g. 1 for last entry in an array, 2 for last entry in an array of structs of arrays, ... */ uint32 ReturnCount : 8; /** Type of reference */ uint32 Type : 4; /** Offset into struct/ object */ uint32 Offset : 20; }; /** uint32 value of reference info, used for easy conversion to/ from uint32 for token array */ uint32 Value; };
- ReturnCount——嵌套深度
- Type——引用的类型(EGCRefenceType)
- Offset——引用对应属性在类中的地址偏移
可以通过解析token流来得到所引用的属性,然后这个属性其实就是这个类的成员变量,换句话说也就是“这个对象所引用的对象”了;其次,两个token(32位)还可以用于存储指针(64位),比如使用AddReferencedObjects添加对象时,它的函数指针。
ProcessObjectArray就是实质性的遍历Uobject的token流并寻找引用关系的方法,在多线程的情况下,UObject的列表被分配给各个线程,并调用ProcessObjectArray。此方法中遍历的就是ObjectsToSerialize中的UObject来寻找引用关系并判断可达性的。
所有一整个标记的流程大概为:UObject的反射信息UClass中含有一个token流属性,这个属性为一个用int值保存属性信息的共同体——>CollectGarbage()中的标记核心函数触发——>PerformReachabilityAnalysis()获取到GUObjectArray中的所有对象,根据标签更改标记,包括继承了FGCObject的非UObject对象,如果对象可达,则放入ObjectsToSerialize——>PerformReachabilityAnalysisOnObjectsInternal()通过解析对象的token流,判断属性是否为UObject类型,如果是,则将引用对象去除不可达标记,并将其加入ObjectsToSerialize中
ObjectsToSerialize这个列表是动态的,因为遍历的过程中肯定会遍历到引用的新对象,于是这个新对象也会被纳入到这个ObjectsToSerialize中,并一轮又一轮得递归遍历。
清除
整个标记流程结束后,GObjectArray再次被遍历,所有的不可达对象放入GUnreachableObjects中。
清除的流程分为UnhashUnreachableObjects和IncrementalDestroyGarbage两部分,前者调用所有不可达对象的BeginDestroy,后者调用所有不可达对象的FinishDestroy。
BeginDestroy意思是通知对象即将被销毁,该对象需要做好在其他线程上的清理工作。
FinishDestroy调用之前会判断对象是否做好清理工作,之后会将走到这一流程的所有对象在下一帧调用析构函数,被彻底清理。
Cluster
//todo
QA
可以看https://zhuanlan.zhihu.com/p/401956734的最后段,我觉得很有用