虚幻4垃圾回收剖析

上一个系列的文章我们已经对虚幻4中的反射实现原理进行了一个简单得讲解,反射的用途非常多,其中一个就是用来做垃圾回收用的,我们这个系列就对虚幻4中的垃圾回收机制做一个讲解。注:本系列文章对应的虚幻4版本是4.14.1

垃圾回收

在计算机科学中,垃圾回收(garbage collection, 缩写GC)是一种自动的内存管理机制。当一个电脑上的动态内存不需要时,就应该予以释放,这种自动内存的资源管理,称为垃圾回收。垃圾回收可以减少程序员的负担,也能减少程序员犯错的机会。最早起源于LISP语言。目前许多语言如Smalltalk、Java、C#、python和D语言等都支持垃圾回收。

下面我们简单的介绍下垃圾回收常见的分类以及实现算法,我们并不会特别细致的去讲,如果读者有兴趣可以自行查找相关的书籍和文献。推荐大家看下参考文献中2的文章。

算法分类

引用计数GC和追踪式GC

引用计数式GC通过额外的计数来实时计算对单个对象的引用次数,当引用次数为0时回收对象。引用计数的GC是实时的。像微软COM对象的加减引用值以及C++中的智能指针都是通过引用计数来实现GC的。

追踪式GC算法在达到GC条件时(强制GC或者内存不够用、到达GC间隔时间)通过扫描系统中是否有对象的引用来判断对象是否存活,然后回收无用对象。

保存式GC和精确式GC

精确式GC是指在回收过程中能准确得识别和回收每一个无用对象的GC方式,为了准确识别每一个对象的引用,通过需要一些额外的数据(比如虚幻中的属性UProperty)。

保存式GC并不能准备识别每一个无用的对象(比如在32位程序中的一个4字节的值,它是不能判断出它是一个对象指针或者是一个数字的),但是能保存在不会错误的回收存活的对象的情况下回收一部分无用对象。保守式GC不需要额外的数据来支持查找对象的引用,它将所有的内存数据假定为指针,通过一些条件来判定这个指针是否是一个合法的对象。

搬迁式和非搬迁式

搬迁式GC在GC过程中需要移动对象在内存中的位置,当然移动对象位置后需要将所有引用到这个对象的地方更新到新位置(有的通过句柄来实现、而有的可能需要修改所有引用内存的指针)。

非搬迁式GC跟搬迁式GC正好相关,在GC过程中不需要移动对象的内存位置。

实时和非实现GC

实时GC是指不需要停止用户执行的GC方式。而非实时GC则需要停止用户程序的执行(stop the world)。

渐进式和非渐进式GC

和实时GC一样不需要中断用户程序运行,不同的地方在于渐进式GC不会在对象抛弃时立即回收占用的内存资源,而在GC达成一定条件时进行回收操作。

回收算法

引用计数式

引用计数算法是即时的,渐近的,对于交互式和实时系统比较友好,因为它几乎不会对用户程序造成明显的停顿

优点:

  • 引用计数方法是渐进式的,它能及时释放无用的对象,将内存管理的的开销实时的分布在用户程序运行过程中。

缺点:

  • 引用计数方法要求修改一个对象引用时必须调整旧对象的引用计数和新对象的引用计数,这些操作增加了指针复制的成本,在总体开销上而言通常比追踪式GC要大。
  • 引用计数要求额外的空间来保存计数值,这通常要求框架和编译器支持。
  • 实际应用中很多对象的生命周期很短,频繁的分配和释放导致内存碎片化严重。内存碎片意味着可用内存在总数量上足够但由于不连续因而实际上不可用,同时增加内存分配的时间。
  • 引用计数算法最严重的问题是环形引用问题(当然可以通过弱指针来解决)。

追踪式GC

追踪式GC算法通过递归的检查对象的可达性来判断对象是否存活,进而回收无用内存。

追踪式的GC算法的关键在于准确并快速的找到所有可达对象,不可达的对象对于用户程序来说是不可见的,因此清扫阶段通常可以和用户程序并行执行。下面主要讨论了算法的标记阶段的实现。

标记清扫(Mark-Sweep)

标记清扫式GC算法是后面介绍的追踪式GC算法的基础,它通过搜索整个系统中对对象的引用来检查对象的可达性,以确定对象是否需要回收。

分类:追踪式,非实时,保守(非搬迁式)或者精确式(搬迁式) ,非渐进

优点:

  • 相对于引用计数算法,完全不必考虑环形引用问题。
  • 操纵指针时没有额外的开销。
  • 与用户程序完全分离。

缺点:

  • 标记清扫算法是非实时的,它要求在垃圾收集器运行时暂停用户程序运行,这对于实时和交互式系统的影响非常大。
  • 基本的标记清扫算法通常在回收内存时会同时合并相邻空闲内存块,然而在系统运行一段时间后仍然难免会生成大量内存碎片,内存碎片意味着可用内存的总数量上足够但实际上不可用,同时还会增加分配内存的时间,降低内存访问的效率。
  • 保守式的标记清扫算法可能会将某些无用对象当做存活对象,导致内存泄露。

用户程序初始化时向系统预申请一块内存,新的对象申请在此区域内分配, 用户程序不需要主动释放己分配的空间,当达到回收条件,或者用户程序主动请求时开始收集内存。

标记清扫式GC算法(mark-sweep)分为两个阶段: 标记阶段 和 清扫阶段。

标记阶段

从根结点集合开始递归的标记所有可达对象。

根结点集合通常包括所有的全局变量,静态变量以及栈区(注2)。这些数据可以被用户程序直接或者间接引用到。

标记前:

标记后:

清扫阶段

遍历所有对象,将没有标记为可达的对象回收,并清理标记位。

保守式的标记清扫算法:

保守式的标记清扫算法缺少对象引用的内存信息(事实上它本身就为了这些Uncooperative Environment设计的),它假定所有根结点集合为指针,递归的将这些指针指向的内存堆区标记为可达,并将所有可达区域的内存数据假定为批针,重复上一步,最终识别出不可达的内存区域,并将这些区域回收。

保守式的GC算法可能导致内存泄漏。由于保守式GC算法没有必需的GC信息,因此必须假设所有内存数据是一个指针,这很可能将一个非指针数据当作指针,比如将一个整型值当作一个指针,并且这个值碰巧在已经分配的堆区域地址范围内,这将会导致这部分内存被标记为可达,进而不能被回收。

保守式的GC不能确定一个内存上数据是否是一个指针,因此不能移动对象的位置。

实际应用:

保守式标记清扫GC算法: Boehm-Demers-Weiser 算法

精确式标记清扫算法:UE3, UE4等

由于我们并不是主要介绍GC算法的,所以接下来我们不打算对其它的GC算法进行细讲,读者可以参考参考文献中第2篇文章或者看垃圾回收的算法与实现这本书,内容比较全面。

标记缩并

有些情况下内存管理的性能瓶颈在分配阶段,内存碎片增加了查找可用内存区域的开销,标记缩并算法就是为了处理内存碎片问题而产生的。

分类:追踪式,非实时,精确式,搬迁式,非渐进

节点复制

节点复制GC通过将所有存活对象从一个区移动到另一个区来过滤非存活对象。

分类:追踪式,非实时,精确式,搬迁式,非渐进

分代式GC(Generational Garbage Collection)

在程序运行过程中,许多对象的生命周期是短暂的,分配不久即被抛弃。因此将内存回收的工作焦点集中在这些最有可能是垃圾的对象上,可以提高内存回收的效率,降低回收过程的开销,进而减少对用户程序的中断。

分代式GC每次回收时通过扫描整个堆中的一部分而是不是全部来降低内存回收过程的开销。

分类:追踪式,非实时,精确式,搬迁式,非渐进

实际应用:Java, Ruby

渐进式GC

渐进式GC要解决的问题是如何缩短自动内存管理引起的中断,以降低对实时系统的影响。

渐进式GC算法基于分代式GC算法,它的核心在于在用户程序运行过程中维护年轻分代的根结点集合。

分类:追踪式,非实时,精确式,搬迁式,渐进式

实际应用:Java, Ruby

虚幻4 中的GC

通过上面我们简单的对GC分类和算法的讲解,再结合虚幻4 的代码,我们可以确定它的GC是追踪式、非实时、精确式,非渐近、增量回收(时间片)。下面我们就从它的UML图、执行流程以及部分代码讲起。

虚幻4中GC的入口是CollectGarbage(),让我们来看一下它的函数原型以及定义

 1 /**
 2
 3 * Deletes all unreferenced objects, keeping objects that have any of the passed in KeepFlags set. Will wait for other threads to unlock GC.
 4
 5 *
 6
 7 * @param    KeepFlags            objects with those flags will be kept regardless of being referenced or not
 8
 9 * @param    bPerformFullPurge    if true, perform a full purge after the mark pass
10
11 */
12
13 COREUOBJECT_API void CollectGarbage(EObjectFlags KeepFlags, bool bPerformFullPurge = true);
14
15 void CollectGarbage(EObjectFlags KeepFlags, bool bPerformFullPurge)
16
17 {
18
19     // No other thread may be performing UOBject operations while we‘re running
20
21     GGarbageCollectionGuardCritical.GCLock();
22
23
24
25     // Perform actual garbage collection
26
27     CollectGarbageInternal(KeepFlags, bPerformFullPurge);
28
29
30
31     // Other threads are free to use UObjects
32
33     GGarbageCollectionGuardCritical.GCUnlock();
34
35 }

从这段代码中我们可以得到如下信息,它是增量式的(bPerformFullPurge),非实时的(gc 锁),最后调用了CollectGarbageInternal来执行真正的垃圾回收操作。

CollectGarbageInternal流程

通过看下面的流程图,我们便可以知道虚幻4垃圾回收就是像我们上面所说的那样,分为标记删除阶段,只不过它多了一个簇(cluster)和增量回收的,而增量回收是为了避免垃圾回收时导致的卡顿的,提出簇的概念是为了提高回收的效率的。

标记阶段(Mark)

虚幻4 中垃圾标记阶段是通过FRealtimeGC类中的PerformReachabilityAnalysis()函数来完成标记的。

 1     /**
 2
 3      * Performs reachability analysis.
 4
 5      *
 6
 7      * @param KeepFlags        Objects with these flags will be kept regardless of being referenced or not
 8
 9      */
10
11     void PerformReachabilityAnalysis(EObjectFlags KeepFlags, bool bForceSingleThreaded = false)
12
13     {
14
15         /** Growing array of objects that require serialization */
16
17         TArray<UObject*>& ObjectsToSerialize = *FGCArrayPool::Get().GetArrayFromPool();
18
19
20
21         // Reset object count.
22
23         GObjectCountDuringLastMarkPhase = 0;
24
25
26
27         // Presize array and add a bit of extra slack for prefetching.
28
29         ObjectsToSerialize.Reset( GUObjectArray.GetObjectArrayNumMinusPermanent() + 3 );
30
31         // Make sure GC referencer object is checked for references to other objects even if it resides in permanent object pool
32
33         if (FPlatformProperties::RequiresCookedData() && FGCObject::GGCObjectReferencer && GUObjectArray.IsDisregardForGC(FGCObject::GGCObjectReferencer))
34
35         {
36
37             ObjectsToSerialize.Add(FGCObject::GGCObjectReferencer);
38
39         }
40
41
42
43         MarkObjectsAsUnreachable(ObjectsToSerialize, KeepFlags);
44
45
46
47         {
48
49             FGCReferenceProcessor ReferenceProcessor;
50
51             TFastReferenceCollector<FGCReferenceProcessor, FGCCollector, FGCArrayPool> ReferenceCollector(ReferenceProcessor, FGCArrayPool::Get());
52
53             ReferenceCollector.CollectReferences(ObjectsToSerialize, bForceSingleThreaded);
54
55         }
56
57         FGCArrayPool::Get().ReturnToPool(&ObjectsToSerialize);
58
59     }

我们可以看到,它首先会调用MarkObjectsAsUnreachable()来把所有不带KeepFlags标记和EinternalObjectFlags::GarbageCollectionKeepFlags标记的对象全部标记为不可达,并把它们添加到ObjectsToSerialize中去。这个函数会判断当前的FUObjectItem::GetOwnerIdnex()是否为0,如果为0那么它就是一个普通物体也就意味着不在簇(cluster)中。把所有符合条件的对象标记为不可达后,然后会调用下面的代码来进行可达性标记。

1        {
2
3             FGCReferenceProcessor ReferenceProcessor;
4
5             TFastReferenceCollector<FGCReferenceProcessor, FGCCollector, FGCArrayPool> ReferenceCollector(ReferenceProcessor, FGCArrayPool::Get());
6
7             ReferenceCollector.CollectReferences(ObjectsToSerialize, bForceSingleThreaded);
8
9         }

下面我们来详细讲解标记的过程,这里会牵扯到我们前面提到的UProperty和UClass也就是我们需要利用反射信息来进行可达性的标记。看上面的调用,有几个比较重要的对象TFastReferenceCollector、FGCReferenceProcessor、以及FGCCollector,下面我们来分别介绍下下面几个类。

TFastReferenceCollector

CollectReferences用于可达性分析,如果是单线程的话就调用ProcessObjectArray()遍历UObject的记号流(token stream )来查找存在的引用,否则会创建几个FCollectorTask来处理,最终调用的还是ProcessObjectArray()函数来处理。下面我们来仔细来讲解一下这个函数。

它会遍历InObjectsToSerializeArray中的UObject对象,然后根据这个类的UClass拿到它的FGCReferenceTokenStream,如果是单线程且bAutoGenerateTokenSteram为true,且没有产生token stream,那么会调用AssembleReferenceTokenStream()来生成,代码如下所示:

 1 // Make sure that token stream has been assembled at this point as the below code relies on it.
 2
 3 if (bAutoGenerateTokenStream && !ReferenceProcessor.IsRunningMultithreaded())
 4
 5 {
 6
 7     UClass* ObjectClass = CurrentObject->GetClass();
 8
 9     if (!ObjectClass->HasAnyClassFlags(CLASS_TokenStreamAssembled))
10
11     {
12
13         ObjectClass->AssembleReferenceTokenStream();
14
15     }
16
17 }

然后它会根据当前的ReferenceTokenSteramIndex来获取FGCReferenceInfo,然后根据它的类型来做相应的操作,代码如下所示:

 1 TokenStreamIndex++;
 2
 3 FGCReferenceInfo ReferenceInfo = TokenStream->AccessReferenceInfo(ReferenceTokenStreamIndex);
 4
 5
 6
 7 if (ReferenceInfo.Type == GCRT_Object)
 8
 9 {
10
11     // We‘re dealing with an object reference.
12
13     UObject**    ObjectPtr = (UObject**)(StackEntryData + ReferenceInfo.Offset);
14
15     UObject*&    Object = *ObjectPtr;
16
17     TokenReturnCount = ReferenceInfo.ReturnCount;
18
19     ReferenceProcessor.HandleTokenStreamObjectReference(NewObjectsToSerialize, CurrentObject, Object, ReferenceTokenStreamIndex, true);
20
21 }
22
23 else if (ReferenceInfo.Type == GCRT_ArrayObject)
24
25 {
26
27     // We‘re dealing with an array of object references.
28
29     TArray<UObject*>& ObjectArray = *((TArray<UObject*>*)(StackEntryData + ReferenceInfo.Offset));
30
31     TokenReturnCount = ReferenceInfo.ReturnCount;
32
33     for (int32 ObjectIndex = 0, ObjectNum = ObjectArray.Num(); ObjectIndex < ObjectNum; ++ObjectIndex)
34
35     {
36
37         ReferenceProcessor.HandleTokenStreamObjectReference(NewObjectsToSerialize, CurrentObject, ObjectArray[ObjectIndex], ReferenceTokenStreamIndex, true);
38
39     }
40
41 }
42
43 ...

在这个处理的过程中,如果新加入的对象数据大于一定数量(MinDesiredObjectsPerSubTask)且是多线程处理,那么就会按需创建一些新的TGraphTask<FCollectorTask>来并行处理引用问题,那么这就是一个递归的过程了。具体的代码读者可以自行去阅读,这里就不展开去讲了。

还记得我们前面一起提到的就是,反射信息用于GC吗?UClass::AssembleReferenceTokenStream()函数就是用生成记号流(token steam,其实就是记录了什么地方有UObject引用),它有一个CLASS_TokenStreamAssembled来保存只需要初始化一次。

这里我们只留一部分的代码,读者可以自行查看AssembleReferenceTokenStream()的定义:

 1 void UClass::AssembleReferenceTokenStream(bool bForce)
 2
 3 {
 4
 5     if (!HasAnyClassFlags(CLASS_TokenStreamAssembled) || bForce)
 6
 7     {
 8
 9         if (bForce)
10
11         {
12
13             ReferenceTokenStream.Empty();
14
15             ClassFlags &= ~CLASS_TokenStreamAssembled;
16
17         }
18
19         TArray<const UStructProperty*> EncounteredStructProps;
20
21         // Iterate over properties defined in this class
22
23         for( TFieldIterator<UProperty> It(this,EFieldIteratorFlags::ExcludeSuper); It; ++It)
24
25         {
26
27             UProperty* Property = *It;
28
29             Property->EmitReferenceInfo(*this, 0, EncounteredStructProps);
30
31         }
32
33         if (GetSuperClass())
34
35         {
36
37             GetSuperClass()->AssembleReferenceTokenStream();
38
39             if (!GetSuperClass()->ReferenceTokenStream.IsEmpty())
40
41                 PrependStreamWithSuperClass(*GetSuperClass());
42
43         }
44
45         else
46
47             UObjectBase::EmitBaseReferences(this);
48
49         static const FName EOSDebugName("EOS");
50
51         EmitObjectReference(0, EOSDebugName, GCRT_EndOfStream);
52
53         ReferenceTokenStream.Shrink();
54
55         ClassFlags |= CLASS_TokenStreamAssembled;
56
57     }
58
59 }

注意,我这里省去了一些代码,其它的大致的逻辑就是如果没有创建token stream或者要强制创建(需要清空ReferenceTokenSteam),那么就会遍历自身的所有属性,然后对每个UProperty调用EmitReferenceInfo()函数,它是一个虚函数,不同的UProperty会实现它,如果它有父类(GetSuperClass()),那么就会调用父类的AssembleReferenceTokenStream()并把父类的添加到数组的前面,同时会处理GCRT_EndofStream的特殊情况,最后加上GCRT_EndOfStream到记号流里面去,并设置CLASS_TokenStreamAssembled标记。

下面我们来看一个UObjectProperty::EmitReferenceInfo的实现,其它的UArrayProperty、UStructProperty等读者可自行查看。

 1 /**
 2
 3 * Emits tokens used by realtime garbage collection code to passed in OwnerClass‘ ReferenceTokenStream. The offset emitted is relative
 4
 5 * to the passed in BaseOffset which is used by e.g. arrays of structs.
 6
 7 */
 8
 9 void UObjectProperty::EmitReferenceInfo(UClass& OwnerClass, int32 BaseOffset, TArray<const UStructProperty*>& EncounteredStructProps)
10
11 {
12
13     FGCReferenceFixedArrayTokenHelper FixedArrayHelper(OwnerClass, BaseOffset + GetOffset_ForGC(), ArrayDim, sizeof(UObject*), *this);
14
15     OwnerClass.EmitObjectReference(BaseOffset + GetOffset_ForGC(), GetFName(), GCRT_Object);
16
17 }

FGCReferenceProcessor

处理由TFastReferenceCollector查找到的UObject引用。

上面的流程图中提到了簇的概念,那么它是用来干什么的呢,我们上面说过它是为了提高GC性能的。我们接下来就来看下簇的概念。

下面我们来看一下UObject的继承关系,其中跟Cluster相关的几个函数在UObjectBaseUtility中,如下图所示:

可以看到,它们都是虚函数,可以被重载,目前来看可以作为簇根(CanBeClusterRoot)的只有UMaterial和UParticleSystem这两个类,而基本上所有的类都可以在簇中(CanBeInCluster),而创建簇是通过CreateCluster来完成的,当然创建簇需要一定的条件,比如我们的CreateClusterFromPackage的函数定义为:

 1 /** Looks through objects loaded with a package and creates clusters from them */
 2
 3 void CreateClustersFromPackage(FLinkerLoad* PackageLinker)
 4
 5 {
 6
 7     if (FPlatformProperties::RequiresCookedData() && !GIsInitialLoad && GCreateGCClusters && !GUObjectArray.IsOpenForDisregardForGC())
 8
 9     {
10
11         check(PackageLinker);
12
13
14
15         for (FObjectExport& Export : PackageLinker->ExportMap)
16
17         {
18
19             if (Export.Object && Export.Object->CanBeClusterRoot())
20
21             {
22
23                 Export.Object->CreateCluster();
24
25             }
26
27         }
28
29     }
30
31 }

FPlatformProperties::RequiresCookedData()代表需要cook好的数据,所以编辑器模式下不会使用簇来GC。

接下来我们看一下CreateCluster()函数的定义:

 1 void UObjectBaseUtility::CreateCluster()
 2
 3 {
 4
 5     FUObjectItem* RootItem = GUObjectArray.IndexToObject(InternalIndex);
 6
 7     if (RootItem->GetOwnerIndex() != 0 || RootItem->HasAnyFlags(EInternalObjectFlags::ClusterRoot))
 8
 9     {
10
11         return;
12
13     }
14
15     // If we haven‘t finished loading, we can‘t be sure we know all the references
16
17     check(!HasAnyFlags(RF_NeedLoad | RF_NeedPostLoad));
18
19     // Create a new cluster, reserve an arbitrary amount of memory for it.
20
21     FUObjectCluster* Cluster = new FUObjectCluster;
22
23     Cluster->Objects.Reserve(64);
24
25
26
27     // Collect all objects referenced by cluster root and by all objects it‘s referencing
28
29     FClusterReferenceProcessor Processor(InternalIndex, *Cluster);
30
31     TFastReferenceCollector<FClusterReferenceProcessor, TClusterCollector<FClusterReferenceProcessor>, FClusterArrayPool, true> ReferenceCollector(Processor, FClusterArrayPool::Get());
32
33     TArray<UObject*> ObjectsToProcess;
34
35     ObjectsToProcess.Add(static_cast<UObject*>(this));
36
37     ReferenceCollector.CollectReferences(ObjectsToProcess, true);
38
39     if (Cluster->Objects.Num())
40
41     {
42
43         // Add new cluster to the global cluster map.
44
45         GUObjectClusters.Add(InternalIndex, Cluster);
46
47         check(RootItem->GetOwnerIndex() == 0);
48
49         RootItem->SetFlags(EInternalObjectFlags::ClusterRoot);
50
51     }
52
53     else
54
55     {
56
57         delete Cluster;
58
59     }
60
61 }

可以看到它也使用了TFastReferenceCollector,只不过这次的模板参数为FClusterReferenceProssor和TCusterCollector,这里我们就不展开去讲了,读者可以自行阅读代码。

FGCCollector

这个类如下图所示是继承自FReferenceCollector,HandleObjectReference()和HandleObjectReferences()都调用了FGCReferenceProcessor的HandleObjectReference()方法来进行UObject的可达性分析。

FGCCollector的UML继承关系图如下所示:

清扫阶段(Sweep)

前面,我们经过了标记过程,那些标记了不可达标记的物体可以进行删除了,为了减少卡顿,虚幻4加入了增量清除的概念(IncrementalPurgeGarbage()函数),就是一次删除只占用固定的时间片,当然,如果是编译器状态或者是强制完全清除(比如下一次GC了,但是上一次增量清除还没有完成,那么就会强制清除)。

IncrementalPurgeGarbage()函数的大体流程如下图所示:

还记得我们前面说的虚幻4使用簇来提高GC的效率吗,下面是CollectGargageInternal函数中的一段,这个时候已经完成了可达性分析,代码如下所示:

 1 for (FRawObjectIterator It(true); It; ++It)
 2
 3 {
 4
 5     FUObjectItem* ObjectItem = *It;
 6
 7     if (ObjectItem->IsUnreachable())
 8
 9     {
10
11         if ((ObjectItem->GetFlags() & EInternalObjectFlags::ClusterRoot) == EInternalObjectFlags::ClusterRoot)// Nuke the entire cluster
12
13         {
14
15             ObjectItem->ClearFlags(EInternalObjectFlags::ClusterRoot | EInternalObjectFlags::NoStrongReference);
16
17             const int32 ClusterRootIndex = It.GetIndex();
18
19             FUObjectCluster* Cluster = GUObjectClusters.FindChecked(ClusterRootIndex);
20
21             for (int32 ClusterObjectIndex : Cluster->Objects)
22
23             {
24
25                 FUObjectItem* ClusterObjectItem = GUObjectArray.IndexToObjectUnsafeForGC(ClusterObjectIndex);
26
27                 ClusterObjectItem->ClearFlags(EInternalObjectFlags::NoStrongReference);
28
29                 ClusterObjectItem->SetOwnerIndex(0);
30
31                 if (!ClusterObjectItem->HasAnyFlags(EInternalObjectFlags::ReachableInCluster))
32
33                 {
34
35                     ClusterObjectItem->SetFlags(EInternalObjectFlags::Unreachable);
36
37                     if (ClusterObjectIndex < ClusterRootIndex)
38
39                     {
40
41                         UObject* ClusterObject = (UObject*)ClusterObjectItem->Object;
42
43                         ClusterObject->ConditionalBeginDestroy();
44
45                     }
46
47                 }
48
49             }
50
51             delete Cluster;
52
53             GUObjectClusters.Remove(ClusterRootIndex);
54
55         }
56
57     }
58
59 }

可以看到,如果UObject带有EInternalObjectFlags::ClusterRoot且不可达,那么它会直接把上面的UObject(Cluster->Objects)符合条件的进行销毁,并且把当前簇删除掉。

总结

到此为止,我们对虚幻4中的垃圾回收进行了大概的讲解,知道了它的GC是追踪式、非实时、精确式,非渐近、增量回收(时间片),先标记后回收的过程,为了提高效率和减少回收过程中的卡顿,可以做到并行标记和增量回收以及通过簇来提高回收的效率等。这篇文章只能给你一个大概的了解,如果想要清楚其中的细节,看代码是免不了的。另外中间有错误的地方,如果读者发现也请指正。欢迎大家留言讨论。

参考文献

  1. https://zh.wikipedia.org/wiki/%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6_(%E8%A8%88%E7%AE%97%E6%A9%9F%E7%A7%91%E5%AD%B8)
  2. http://www.cnblogs.com/superjt/p/5946059.html
时间: 2024-10-12 17:51:34

虚幻4垃圾回收剖析的相关文章

搞懂Go垃圾回收

本文主要介绍了垃圾回收的概念,Golang GC的垃圾回收算法和工作原理,看完本文可以让你对Golang垃圾回收机制有个全面的理解.由于本人不了解其他语言的GC,并未对比其他语言的垃圾回收算法,需要的可以自行Google. 什么是垃圾回收 垃圾回收(英语:Garbage Collection,缩写为GC),在计算机科学中是一种自动的存储器管理机制.当一个计算机上的动态存储器不再需要时,就应该予以释放,以让出存储器,这种存储器资源管理,称为垃圾回收.垃圾回收器可以让程序员减轻许多负担,也减少程序员

Java性能剖析]Sun JVM内存管理和垃圾回收

内存管理和垃圾回收是JVM非常关键的点,对Java性能的剖析而言,了解内存管理和垃圾回收的基本策略非常重要.本篇对Sun JVM 6.0的内存管理和垃圾回收做大概的描述. 1.内存管理      在程序运行过程当中,会创建大量的对象,这些对象,大部分是短周期的对象,小部分是长周期的对象,对于短周期的对象,需要频繁地进行垃圾回收以保证无用对象尽早被释放掉,对于长周期对象,则不需要频率垃圾回收以确保无谓地垃圾扫描检测.为解决这种矛盾,Sun JVM的内存管理采用分代的策略.      1)年轻代(Y

《python解释器源码剖析》第17章--python的内存管理与垃圾回收

17.0 序 内存管理,对于python这样的动态语言是至关重要的一部分,它在很大程度上决定了python的执行效率,因为在python的运行中会创建和销毁大量的对象,这些都设计内存的管理.同理python还提供了了内存的垃圾回收(GC,garbage collection),将开发者从繁琐的手动维护内存的工作中解放出来.这一章我们就来分析python的GC是如何实现的. 17.1 内存管理架构 在python中内存管理机制是分层次的,我们可以看成有四层,0 1 2 3.在最底层,也就是第0层是

(转载)虚幻引擎3--UE3垃圾回收机制

原文地址 要进行垃圾回收,有两个条件:一.要知道所有的对象放在哪里,即内存中的位置.二.要知道对象的这块内存上,数据表示的是什么意思,是一个Float数还是Int数还是一个对象指针的值.具备这两个条件,才可以遍历所有的对象,找出没有被引用的对象,然后删除释放掉. U3是如何满足这两个条件的呢,先看第一个. U3里几乎所有的类,都以UObject为基类,一般核心基类都尽量做得简洁明了,但UObject却很复杂,有大量的static成员变量和成员函数,来实现包括对象构造.对象管理.垃圾回收.资源加载

Java 垃圾回收机制概述

摘要: Java技术体系中所提倡的 自动内存管理 最终可以归结为自动化地解决了两个问题:给对象分配内存 以及 回收分配给对象的内存,而且这两个问题针对的内存区域就是Java内存模型中的 堆区.关于对象分配内存问题,笔者的博文<JVM 内存模型概述>已经阐述了 如何划分可用空间及其涉及到的线程安全问题,本文将结合垃圾回收策略进一步给出内存分配规则.垃圾回收机制的引入可以有效的防止内存泄露.保证内存的有效使用,也大大解放了Java程序员的双手,使得他们在编写程序的时候不再需要考虑内存管理.本文着重

golang 垃圾回收 gc

http://ruizeng.net/golang-gc-internals/ 摘要 在实际使用go语言的过程中,碰到了一些看似奇怪的内存占用现象,于是决定对go语言的垃圾回收模型进行一些研究.本文对研究的结果进行一下总结. 什么是垃圾回收? 曾几何时,内存管理是程序员开发应用的一大难题.传统的系统级编程语言(主要指C/C++)中,程序员必须对内存小心的进行管理操作,控制内存的申请及释放.稍有不慎,就可能产生内存泄露问题,这种问题不易发现并且难以定位,一直成为困扰开发者的噩梦.如何解决这个头疼的

浅析JAVA的垃圾回收机制(GC)

1.什么是垃圾回收? 垃圾回收(Garbage Collection)是Java虚拟机(JVM)垃圾回收器提供的一种用于在空闲时间不定时回收无任何对象引用的对象占据的内存空间的一种机制. 注意:垃圾回收回收的是无任何引用的对象占据的内存空间而不是对象本身.换言之,垃圾回收只会负责释放那些对象占有的内存.对象是个抽象的词,包括引用和其占据的内存空间.当对象没有任何引用时其占据的内存空间随即被收回备用,此时对象也就被销毁.但不能说是回收对象,可以理解为一种文字游戏. 分析: 引用:如果Referen

Python 中的垃圾回收机制

GC作为现代编程语言的自动内存管理机制,专注于两件事:1. 找到内存中无用的垃圾资源 2. 清除这些垃圾并把内存让出来给其他对象使用.GC彻底把程序员从资源管理的重担中解放出来,让他们有更多的时间放在业务逻辑上.但这并不意味着码农就可以不去了解GC,毕竟多了解GC知识还是有利于我们写出更健壮的代码. 引用计数 Python语言默认采用的垃圾收集机制是『引用计数法 Reference Counting』,该算法最早George E. Collins在1960的时候首次提出,50年后的今天,该算法依

V8垃圾回收?看这篇就够了!

什么是内存管理 内存管理是控制和协调应用程序访问电脑内存的过程.这个过程是复杂的,对于我们来说,可以说相当于一个黑匣子. 当咱们的应用程序运行在某个操作系统中的时候,它访问电脑内存(RAM)来达成下列几个功能: 运行需要执行的字节码(代码) 存储程序运行时候所需的数据 加载程序运行所需的运行时环境 上面用来存储程序运行时所需的数据,就是下面要说的堆(heap)和栈(stack). 栈(stack) 顾名思义,是一种先进后出的结构,参考一下餐盘的取和放. 俄罗斯套娃,我这不禁 栈的特点 由于先进后