大象无形UE笔记十二:垃圾回收

大象无形UE笔记十二:垃圾回收

概述

接上节 UObject 。本节主要介绍UE引擎的对象销毁,以及垃圾回收(GC)相关的知识,对应原书的10.1.3,10.1.4节。让我们开始吧!

释放与消亡

销毁过程

UObject对象无法手动释放,只能手动请求“ConditionalBeginDestroy”来申请销毁。具体是否销毁,何时销毁,取决于虚幻引擎的垃圾回收系统。实际上ConditionalBeginDestroy函数只是设置了UObject的RF_BeginDestroyed为真,并通过SetLinker函数将当前对象从Linker导出表中清除。

待垃圾回收确认销毁该UObject时,由FinishDestroy函数完成UObject销毁操作。首先使用函数DestroyNoNativeProperties销毁非C++的成员变量。其核心是一个for循环,获取当前UClass类的析构函数链表,调用每个析构函数的DestroyValue_InContainer函数,以完成自身的销毁。

for (UProperty *p = GetClass()->DestructorLink; p; p=p->DestructorLinkNext)
{
    p->DestroyValue_InContainer(this);
}

触发销毁

销毁主要由虚幻引擎的垃圾回收器来触发。

垃圾回收器实际上执行两个步骤:析构和回收。前者负责调用析构函数,通知对象进行析构操作。后者则负责回收当前UObject占用的内存。相关代码在baseCollection.cpp的IncrementalPurgeGarbage中,如下图:

如上图,第一阶段为通知各UClass父类进行析构阶段,后阶段为C++析构和内存回收阶段。

垃圾回收

垃圾回收的算法比较复杂,这里就不深入探讨源码实现,而是只是对垃圾回收过程进行简单的介绍。

垃圾回收算法简介

垃圾回收算法简单来说是解决这样一个问题:在一个合租房内公共区域里面的物品,每个人原则上会去收拾自己的垃圾(父对象负责回收子对象的释放)。但在丢东西时,不知道别人有没有在用,贸然丢弃就会引起问题。比如我认为这个属于我的电磁炉已经不需要了,于是随手丢进了垃圾堆。过了一会儿舍友从房间出来,准备煮面,发现电磁炉没了(在别的对象持有当前对象引用情况下释放对象,导致野指针)。

于是大家只好采取一个保守的策略,大家都不丢垃圾,屋子里堆满了垃圾,需要一个垃圾回收系统来确定哪些垃圾可以丢。比较典型的垃圾回收算法有两种:引用计数 和 标记-清扫两种:

引用计数算法

给每个东西都挂一个数字牌,我要用时,就把数字加1,不需要时候就把数字减1。一旦最后一个人不用这个东西时,发现减1时为0,于是把这个东西丢进垃圾桶。

优势:垃圾回收操作是实时进行,不需要暂停其他人的工作。

劣势:1,指针操作开销,每次操作都要调整数字牌的数字。2,环形引用,比如锅盖引用锅,锅引用锅盖,导致两者一直未能释放。

标记-清扫算法

标记-清扫算法时追踪式GC的一种。会寻找整个对象引用网络,来寻找不需要垃圾回收的对象。这与引用计数的“只关心单个对象”思路相反。先假定所有东西都是垃圾,然后让每个舍友(在算法中称为根)开搜寻,让每个舍友指认哪些东西时他/她需要的,最终剩下没有指认的东西,就是垃圾,可以直接回收。

优势无环形引用。当没有人需要用锅时,由于锅和锅盖没有任何舍友(根)引用,所以都会被回收掉。

劣势需暂停。算法开始执行时,需要暂停舍友们的正常工作,等算法结束后,大家才能继续做手头的事情。

算法的五个维度

在垃圾算法的分类中有如下五个维度:

引用计数/追踪式之前已经大致介绍过了。

保守/精确式的区别主要在于,保守式只释放那些“绝对不可能被引用”的对象,不求全部回收。而精确式则需要添加额外信息来进行辅助识别指针字段,能精确识别每一个被引用的对象。

搬迁/非搬迁式的区别是对象在GC过程中内存的位置是否进行移动,移动的好处是方便处理内存碎片,坏处是需要修正大量指针地址。

实时/非实时的区别是:实时GC不需要中断用户程序进行,非实时GC则要求程序暂停来进行垃圾回收。

渐进/非渐进的区别是:渐进式是逐步完成搜索与回收,非渐进式需要一口气完成搜索和回收操作。

UObject的标记清扫算法

虚幻引擎的垃圾回收算法的特点如下:

1,虚幻引擎的UObject的反射系统已经提供每个对象的互相引用的信息,从而能够实现对象引用网络。故采用追踪式GC。

2,UClass包含了类的成员信息,类的成员变量也包含了“是否是指向对象的指针”的信息,因此具备精确式GC的客观条件。也就是利用反射系统,完成对每一个被引用对象的定位,所以选择精确式GC。

3,虚幻引擎回收过程中没有搬迁对象,因为搬迁时修正指针地址的庞大成本。

4,虚幻引擎选择了一个非实时但是渐进式的垃圾回收算法。将垃圾回收的过程分布,并行化,以削弱选择追踪式GC带来的停等消耗。

也就是说UE引擎的垃圾回收的五个维度如下:

标记-清扫步骤

清扫步骤我们会逐步补全和细化如下图:

如上,首先借助GGarbageCollectionGuradCritial类的CGLock函数,我们可以在垃圾回收期间,锁定其他线程对UObject的操作,避免一边回收一边修改或操作UObject带来冲突问题。然后在回收结束后,再调用GCUnlock解锁它。

回收过程对应函数为FRealtimeGC::performReachablilityAnalysis,主要有:标记和清除 两个步骤。 标记的过程为:全部标记不可达,然后遍历对象引用网络来标记可达对象。清除的过程也很简单,直接检查标记,对没有标记可达的对象调用ConditioanalBeginDestroy函数来请求删除:

全部标记不可达的算法很简单,虚幻引擎使用MarkObjectAsUnreachable函数可以标记一个对象不可达,可以用FRawObjectIterator来遍历所有的Object,并设置Unreachable标记即可。

如上图,我们把所有对象都标记为“Unreachable”,即把它涂成红色。

那么我们从那里开始遍历呢?再标记完所有对象不可达之后,我们需要把所有的“必然可达”对象都收集到数组中。这些必然可达的对象刚开始是哪些被添加到“根”的对象。(可以通过AddToRoot,把一个UObject添加到根对象)。这些对象是一定不会被垃圾回收的,相当于前面举例的“舍友”。如下图:

如上,那三个根对象已经被我涂成绿色了,表示对象“可达”。

接下来,会遍历这些必然可达对象,然后搜索这些必然可达对象能够达到的其他对象,从而标记所有可达对象。这些可达对象就好像渔网最上面的一条浮子,浮子下面的就是一大堆的互相引用的对象。

标记完所有可达对象之后,剩下的“不可达”对象就可以被垃圾回收了。

总所周知,对于有向图的遍历比较复杂,而虚幻引擎需要一个并行化的对象引用的遍历方案。于是它把二维的结构一维化了。它在UClass那里使用了一个FGCReferenceInfo为成员的流FGCReferenceTokenStream数组来保存这个类的对象的引用信息。其中这个FGCReferenceInfo是一个32位Union结构,主要有如下信息:

ReturnCount 8位 返回的深度计数,用于统计需要返回的深度

Type 类型:可以是 GCRT_Object(对象) / GCRT_Class(类) / GCRT_PersistentObject(永久对象)等

Offset:当前成员在对象内存的偏移结构

我们来打印一个之前我们测试用的UPlayerObject的UClass对应的FGCReferenceTokenStream数据信息:

可以看到它的第0个元素:偏移16是一个Class类型的引用, 第1个元素 偏移40是一个 Outer的引用。而元素7是一个偏移为72的 Lover的Object的引用,我们可以看到它的数据结构如下:

这些刚好对应UObject里面引用其他对象的几个成员。

当这个FGCReferenceTokenStream交给TFastReferenceCollector进行处理时,这个Collector通过不断遍历这个线性结构,调用FGCReferenceProcessor来处理每一个引用。

FGCReferenceProcessor是具体设置每一个对象标记的类。其HandleObjectReference函数具体处理对象引用,首先会将当前引用目标对象的可达性设置为可达,然后放入前文所述的必然可达数组中。

这个涉及的核心目的是为了加速:由于线性结构遍历时非常容易进行并行化的,所以可以在合适时机将垃圾回收任务进行并行化。遍历Token时,会根据实际情况,将前面提到的必然可达数据分为几个段落。对每个段落进行并行化方式进行加速遍历。

清扫算法比标记算法更为简单。虚幻引擎采取了增量清扫的算法,对应的函数为IncrementalPurgeGarbage函数。所谓的增量清扫,是指循环引擎会考虑时间限制等,一段一段进行销毁的触发。由于可能会在两次清扫时间之间产生新的UObject,所以在每次进入清扫时,会检查GObjCurrentPurgeObjectIndexNeedsReset,并根据实际情况,重新生成对象迭代器,避免上次记录的迭代器失效。

总结

好的,虚幻引擎的垃圾回收就先讲到这里了。这一次我们没有深入解析代码,而是从原理层面大致叙述了垃圾回收的概念和过程。对于想深入了解垃圾回收源码的同学,我推荐 UE小学徒 大佬写的 这篇 UE4-GarbageCollect垃圾回收机制

下一节我们可能会分享Actor对象的产生,加载与释放。也可能直接进入渲染系统的分享。敬请期待!

发布于 2022-12-29 23:50・IP 属地广东