ue gc
前言
本篇以源码角度解析UE中标记清除算法GC的实现
https://zhuanlan.zhihu.com/p/401956734
UE 4.26
垃圾回收伪实现
标记清除算法 、 复制算法 、标记-整理算法 、分代收集算法等。我们就用标记清除算法来实现。标记-清除算法,看名字就知道有两个阶段,标记和清除:
- 标记:遍历所有对象,根据某种规则,标记其是否需要清除
- 清除:遍历所有对象,清除标记了的对象,回收内存
因此可知,要实现标记清除垃圾回收,在标记阶段我们需要做到以下两点:
- 能拿到所有对象
- 确定对象清除的规则
在自定义的 NewObject 方法内,把生成的对象指针放入全局数组 GUObjectArray ,这样我就能拿到所有对象了
以下就是垃圾回收的伪实现:
- 启动垃圾回收,加锁( 保持所有对象的引用关系不变 )
- 设置所有对象为”不可达”标记(根对象、特殊对象 除外)
- 遍历根对象列表,根对象引用到的对象去除”不可达”标记
- 收集所有仍然标记为”不可达”的对象,全部删除
GC过程
GC 启动
- 手动:UWorld::ForceGarbageCollection( bool bFullPurge)会在World.tick 的下一帧强行进行GC
- 系统会根据默认的设置(可重新配置)一定的间隔时间或者条件下,自动调用垃圾回收
GC锁
GC锁的主要用处就是为了暂停其他线程以免UObject对象的引用关系在GC过程中发生变化。
1 | void CollectGarbage(EObjectFlags KeepFlags, bool bPerformFullPurge) |
- 发送信号,表示我想获取GC锁,GCWantsToRunCounter 自增(原子操作)
- GC 线程 Sleep,查看 AsyncCounter 是否等于 0 判断其他线程是否有阻塞GC的操作还在执行,不等于 0 就继续等待
- AsyncCounter = 0,通过另一个变量 GCCounter 递增(原子操作),来标识正在执行GC,其他所有线程将被阻塞
- 执行内存屏障
- 将 GCWantsToRunCounter 设为 0,开始真正的 GC 操作
- GC 操作完毕, GCCounter 自减释放 GC 锁
内存屏障的主要意思就是,在这个屏障之前的所有读和写的操作,一定会在这个屏障后面的读和写的操作之前执行。为了防范多线程读写操作时序问题导致的逻辑 bug,详细内容自行 Google
获取所有对象
NewObject的时候把生成的对象的指针放入一个全局数组。
这边GUObjectArray 不是数组,就是一个容器体,为了多线程分块扫描
1 | //UObjectBase.cpp |
UObject对象不直接存入容器的,而是被组装成 FUObjectItem 结构:
1 | //UObjectArray.h |
对与上面的Flags的标识 有:
1 | //ObjectMacros.h |
标记不可达
1 | //GarbageCollection.cpp |
所以 非UObject 对象的 FMyStruct 类继承与FGCObject 可以变成可GC
1 | class FMyStruct: public FGCObject |
引用关系分析
MarkObjectsAsUnreachable() 方法消耗不大,因为是多线程操作,且这些 FUObjectItem 结构体对象是内存块连续的数据,对CPU很友好。
怎么去处理对象的引用依赖关系呢?
遍历:通过UObject对象的实例化地址就可以将相应的属性遍历出来,进行读写,等于就是遍历了一整个引用树,这样效率过低。
所以在反射生成阶段就已经用ReferenceTokenStream收集到Token数组中了。
在 https://zhuanlan.zhihu.com/p/58868952有提到这个结构。
1 | struct FGCReferenceInfo |

PerformReachabilityAnalysisOnObjectsInternal 代码内最后调用的分析代码是 ProcessObjectArray
代码看原文
HandleTokenStreamObjectReference 会调用 HandleObjectReference,它会去除它的”不可达”标记,并将它加入NewObjectsToSerialize,开辟新的 task 线程去处理,而不是在当前线程递归
清理
遍历 GObjectArray内的数组仍然被标记为不可达的对象放入 GUnreachableObjects ,随后就是执行清除
清理有分俩个逻辑
- UnhashUnreachableObjects:调用不可达对象的ConditionalBeginDestroy()方法,最终会调用 BeginDestroy()
- 通知UObject对象,告知这个对象即将被销毁,销毁之前需要做什么事情这是最后的机会
- IncrementalDestroyGarbage:调用不可达对象的ConditionalFinishDestroy()方法,最终会调用 FinishDestroy()
- 内部其实也没有真正销毁对象,因为没有调用对象的析构函数
- TickDestroyGameThreadObjects 调用析构方法的地方
- 根据GC的设置在下一帧或者规则的时间内执行
1 | //标记RF_BeginDestroyed |
因此,GC以后可能会出现的情况是:isVaild(Obj) 仍为 true, 实际上这个对象已经被标记为 PendingKill,只是还未置空。因此可使用 isVaildLowLevel(Obj) 来判断更准确
整个分析过程中,我省略了GC锁操作,多线程分析引用等,是为了更专注分析垃圾回收的流程;其实省略的最大一个块就是:Cluster。为什么需要Cluster ? 因为在游戏过程中很多对象的生命周期一致,是命运共同体。比如:粒子内的一堆东西其实就可以当成一个”对象”来处理,能加快分析速度。
总结
1 | void CollectGarbageInternal(EObjectFlags KeepFlags, bool bPerformFullPurge) |
GC问答
了解了GC的流程,你觉得应该怎么优化GC?
- 打开簇,将Character,Weapon等生命周期一致的 Actor 对象勾选 Cluster
- 最好的优化还是减少UObject对象数量(包括:少用蓝图宏,Level内的Actor数量控制)
- 优化GC调用时机,原则上能不调用就不调用,可在关键点调用
- 采用对象池,不要频繁清理和生成大的对象
- 优化源代码,将可达性分析这块看看能不能改成无锁的方式,加快速度
源码阅读
UE5.2
- UObjectBasse::ProcessNewlyLoadedUObjects() 中会调用Class->AssembleReferenceTokenStream();
- 引用关系分析开始
- 为GC做准备
- 遍历类型中所有FProperty->EmitReferenceInfo(..);
- 会设置类型 偏移量->加入Tokens 中
- 入口
- TryCollectGarbage()
- 和CollectGarbage() 差别就是能不能获取GC锁
- Try有上限 Try10次的话都获取失败的话就会强制 Force GC
- CollectGarbage()
- CollectGarbageInternal() 上文有提到
- CollectGarbageImpl<>()
- !IsLoading return
- 执行Flush 等待所有异步加载结束
- 可达性分析 PerformReachabilityAnalysis()
- 找出根节点 MarkObjectsFunctions()
- 找到Root节点以及flag对象 放入函数指针数组 获取所有对象
- 将root对象放入 LocalObjectsToSerialize[] 中
- 其他对象组织簇 Cluster objects
- 分析不需要GC的Flags对象 这些对象会标记不可达 并加一个Flag
- 可达对象加入 LocalObjectsToSerialize[] 中
- 每个线程的 LocalObjectsToSerialize[] 会加到 总数组 ObjectsToSerializeArrays[] 中
- 遍历每个Root节点 PerformReachabilityAnalysisOnObjects()
- 多线程与普通的遍历 Root[] 调用ProcessObjectArray()
- 取出与遍历TokenStream[](第一步反射时写入内存的)
- 最后调用到把对象设置为可达对象
- 多线程与普通的遍历 Root[] 调用ProcessObjectArray()
- 把不可达对象放到一个数组中GUnreachableObjects[]
- 找出根节点 MarkObjectsFunctions()
- 清理
- UnhashUnreachableObjects
- UnhashUnreachableObjects -> 调用BeginDestory()
- IncrementalPurgeGarbage -> IncrementalDestroyGarbage()
- 对每个不可达对象调用ConditionalFinishDestroy()->调用FinishDestroy()
- 会进行俩次while 因为可能多线程会某些obj依旧在加载中第二次while会等待渲染线程完成
- TickPurge
- TickDestroyGameThreadObjects()调用 obj析构函数
- 这里会清理内存碎片 省略
- UnhashUnreachableObjects

