UE UObject 与 GC 1

概述

在进入GC流程前,我觉得需要普及一些概念。。。

对象簇

FUObjectCluster

把一组紧密相关的 UObject 打包成一个整体,GC 时只检查"包"而不检查里面每个对象,大幅减少遍历量

假设加载一个材质包,里面有 UMaterial + 50 个 UMaterialExpression + 若干 Shader 相关对象。没有簇时,GC 需要逐个遍历这 50+ 个对象的引用。但这些对象的生死完全由材质决定——材质活着它们就活着,材质死了它们也该死。逐个遍历纯属浪费。

簇就是把它们捆成一个单元:簇根(Cluster Root)可达 → 整个簇可达,不需要逐个遍历成员。

struct FUObjectCluster
{
    int32 RootIndex;                    // 簇根对象索引(如 UMaterial)
    TArray<int32> Objects;              // 簇内成员对象索引(同包、可簇化的对象)
    TArray<int32> ReferencedClusters;   // 本簇引用的其他簇的根索引
    TArray<int32> MutableObjects;       // 不能入簇但被引用的对象(需单独遍历)
    TArray<int32> ReferencedByClusters; // 引用本簇的其他簇(用于级联溶解)
    bool bNeedsDissolving;             // 标记需要溶解
};

什么对象能当簇根 / 入簇

  • 簇根。重写 CanBeClusterRoot() 返回 true 的类:UMaterialUBlueprintGeneratedClassUSoundCue
  • 入簇成员。CanBeInCluster() 返回 true(默认委托给 Outer 链)、在同一个包内、已加载完成、非根对象
  • 不能入簇。不同包、未加载完成、被 AddToRoot() 锁定、CanBeInCluster() 返回 false → 放入 MutableObjects

注意:簇只在 Cooked 构建(打包后的游戏)中创建,编辑器中不会创建。

创建时机:

资源加载完成后(PostLoad),引擎检查每个对象是否 CanBeClusterRoot(),是就调用 CreateCluster()

加载材质包
  → PostLoad 完成
  → UMaterial::CanBeClusterRoot() == true
  → UMaterial::CreateCluster() // 这个函数可能在基类 UObjectBaseUtility::CreateCluster
      → FClusterReferenceProcessor 遍历所有引用
          ├─ 同包、可簇化的对象 → Cluster.Objects(成员)
          ├─ 已属于其他簇的对象 → Cluster.ReferencedClusters(簇间引用)
          ├─ DisregardForGC 对象 → 忽略
          └─ 不同包/不可簇化    → Cluster.MutableObjects(需单独追踪)
      → 成员数 < MinClusterSize → 簇太小,丢弃

GC 期间的处理:

性能收益:一个有 100 个成员的簇,GC 只需处理 1 个簇根 + 少量簇间引用和 MutableObjects,而不是 100 个对象的引用遍历。

簇溶解:

  • GC 期间:簇根是垃圾 → DissolveClusterAndMarkObjectsAsUnreachable(),成员标记为 MaybeUnreachable
  • 运行时:簇根或成员被销毁 → DissolveCluster(),清除成员的 OwnerIndex
  • 级联溶解:如果簇 A 引用簇 B,簇 B 溶解时会通过 ReferencedByClusters 级联溶解簇 A

关卡 Actor 簇(特殊处理):

ULevel 不直接当簇根,而是创建一个 ULevelActorContainer 作为簇根,把关卡中可簇化的 Actor 全部装入,不可簇化的 Actor 放入 ActorsForGC 单独追踪。

如何标记为垃圾(Garbage)

// UObjectBaseUtility.h
FORCEINLINE void MarkAsGarbage()
{
    check(!IsRooted());
    AtomicallySetFlags(RF_MirroredGarbage);
    GUObjectArray.IndexToObject(InternalIndex)->SetGarbage();  // 设置 bit 21
}

这是 UE5 替代旧版 MarkPendingKill() 的接口。当引擎/游戏代码决定某个对象"逻辑上已死"时调用。比如 Actor 被 Destroy() 时,最终会调到 MarkAsGarbage()

剧透: GC流程会找出一些根,一定不会被回收的东西,然后顺着做可达性查询。

  • 这种标记为垃圾的对象就不会被标记为根。
  • 遍历引用时发现指向 Garbage 对象的引用,直接 null 掉
  • 簇根是 Garbage → 整个簇溶解

GUObjectArray

GUObjectArray 是 UE 中所有 UObject 的全局注册表——每个 UObject 创建时都会在这个数组里占一个槽位。

核心结构

class FUObjectArray;

GUObjectArray (FUObjectArray)
│
├── ObjObjects (FChunkedFixedUObjectArray)
│   ├── Chunk 0: [FUObjectItem × 65536]  ─┐
│   ├── Chunk 1: [FUObjectItem × 65536]   │ DisregardForGC 区(永不扫描)
│   ├── ...                               ─┘  索引 0 ~ ObjLastNonGCIndex
│   ├── Chunk N: [FUObjectItem × 65536]  ─┐
│   └── ...                               │ GC 管辖区
│                                         ─┘  索引 ObjFirstGCIndex ~ 末尾
│
├── ObjAvailableList: [可复用的空闲索引]
├── ObjFirstGCIndex:  GC 扫描的起始索引
└── ObjLastNonGCIndex: DisregardForGC 区的末尾索引
struct FUObjectItem
{
    UObjectBase* Object;     // 指向 UObject 的指针
    int32 Flags;             // EInternalObjectFlags(Garbage、Reachable、ClusterRoot 等)
    int32 ClusterRootIndex;  // 簇索引(负值)或 Owner 索引(正值)
    int32 SerialNumber;      // TWeakObjectPtr 用的序列号,防止悬空引用
    int32 RefCount;          // 引用计数,>0 时阻止 GC 回收
};

UObject 自身的 InternalIndex 字段就是它在这个数组中的下标。通过 GUObjectArray.IndexToObject(Index) 即可 O(1) 访问元数据。

两个区域

  • DisregardForGC
    • 0 ~ ObjLastNonGCIndex
    • 引擎初始化期间创建的对象(CDO、引擎单例等),GC 永远不扫描。性能优化——这些对象生命周期等同进程
  • GC 管辖区
    • ObjFirstGCIndex ~ 末尾
    • GC 每次从这里开始扫描。游戏运行时创建的对象都在这里

初始加载期间 OpenForDisregardForGC = true,新对象进入 DisregardForGC 区。加载完成后调用 CloseDisregardForGC() 关闭,之后的新对象都进入 GC 管辖区。编辑器中此优化被禁用(MaxObjectsNotConsideredByGC = 0)。

对象的增删

创建函数(AllocateUObjectIndex):

  1. 优先从空闲列表 ObjAvailableList 复用已释放的索引
  2. 没有空闲索引则在数组末尾追加

销毁函数(FreeUObjectIndex):

  1. 清空槽位(Object 指针置 null、Flags 归零)
  2. GC 区的索引回收到 ObjAvailableList;DisregardForGC 区的不回收

FChunkedFixedUObjectArray 是什么

class FUObjectArray
{
    typedef FChunkedFixedUObjectArray TUObjectArray;
    TUObjectArray ObjObjects;
}

为什么用分块数组 FChunkedFixedUObjectArray

这里面维护了一个二维数组 FUObjectItem。每一行是 n = 10 * 1024 个 FUObjectItem

扩容就一次扩一行。寻址就 Index / n 第几行,在取余第几个。在operator [] 里面处理。

对多线程有什么意义?

  • 游戏线程分配新 Chunk 时(扩容),只写 Objects[N] = 新Chunk指针(一次原子写)
  • 已有 Chunk 的地址不变,GC 线程持有的指针仍然有效
  • 读已有索引永远安全,不需要加锁
  • 只有写操作(添加新对象)需要加锁,而且只锁很短的时间(AllocateUObjectIndex函数里面)

=====

GC 标记阶段直接遍历 GUObjectArrayObjFirstGCIndex 开始的所有槽位,读取每个 FUObjectItemFlags 判断可达性。这就是代码中各种 GUObjectArray.GetObjectItemArrayUnsafe()[Index] 调用的来源。TWeakObjectPtr 也基于此——用 (Index, SerialNumber) 对来安全地引用对象,SerialNumber 不匹配则说明对象已被销毁。

=====

这一趴有理解什么呢?

UObject 会挂在一个全局数组上,这个数组分段了,前一段GC不扫描,来放一些CDO,引擎单例这些生命周期就是进程。后面是需要GC的对象。

UObject 自身的 InternalIndex 字段就是它在这个数组中的下标。通过 GUObjectArray.IndexToObject(Index) 即可 O(1) 访问元数据。

这个元数据就是 FUObjectItem

怎么拿的给看这个的 operator[] 重载,里面有一些chunk分块逻辑

GC的入口

E:\Epic\UE_5.6\Engine\Source\Runtime\CoreUObject\Private\UObject\GarbageCollection.cpp

UE::GC::CollectGarbageInternal(KeepFlags, bPerformFullPurge);

然后就到了 FReachabilityAnalysisState::CollectGarbage

GReachabilityState 是一个全局对象

class FReachabilityAnalysisState
{
private:
    // 工作线程数,以及上下文对象
    int32 NumWorkers = 0;
    FWorkerContext* Contexts[MaxWorkers] = { nullptr };

    // 用于启动 UObject 可达性分析溯源的附加对象标志
    EObjectFlags ObjectKeepFlags = RF_NoFlags;

    // 如果是True,拿这个就是一次全量的GC
    bool bPerformFullPurge = false;

    // 可达性分析,由于时间片被挂起了就是True
    bool bIsSuspended = false;

    // 上次可达性分析运行的统计数据(所有迭代)
    UE::GC::FProcessorStats Stats;

    // 所有可达性分析迭代的总时长
    double IncrementalMarkPhaseTotalTime = 0.0;

    // 实际引用遍历的总时长
    double ReferenceProcessingTotalTime = 0.0;

    // 可达性分析过程中执行的可达性分析迭代次数
    int32 NumIterations = 0;

    // 启用 gc.DelayReachabilityIterations 运行时,需要跳过的可达性分析迭代次数
    int32 NumRechabilityIterationsToSkip = 0;
void FReachabilityAnalysisState::CollectGarbage(EObjectFlags KeepFlags, bool bFullPurge)
{
    using namespace UE::GC::Private;

    // [1] 处理增量可达性被打断
    if (GIsIncrementalReachabilityPending)
    {
        bPerformFullPurge = true;
        PerformReachabilityAnalysisAndConditionallyPurgeGarbage(/*bReachabilityUsingTimeLimit =*/ false);
        checkf(!GIsIncrementalReachabilityPending, TEXT("Flushing incremental reachability analysis did not complete properly"));
        AcquireGCLock(); // PostCollectGarbageImpl 中释放了锁,需要重新获取
    }

    // [2] 保存本轮参数
    ObjectKeepFlags = KeepFlags;
    bPerformFullPurge = bFullPurge;

    // [3] 决定是否使用时间限制
    const bool bReachabilityUsingTimeLimit = !bFullPurge && GAllowIncrementalReachability;
    PerformReachabilityAnalysisAndConditionallyPurgeGarbage(bReachabilityUsingTimeLimit);
}
  1. 增量上一轮增量可达性分析(Incremental Reachability Analysis)在某帧挂起后、尚未完成时,又有新的 CollectGarbage 请求进来。处理方式:强制升级为 Full Purge(bPerformFullPurge = true),不使用时间限制一口气跑完剩余分析 + 清除不可达对象。完成后 checkf 断言验证增量状态已清除。因为 PostCollectGarbageImpl 内部释放了 GC 锁,所以需要 AcquireGCLock() 重新获取。
  2. ObjectKeepFlags(带哪些标志的对象视为根,如编辑器中 RF_Standalone)和 bPerformFullPurge(是否完整清除)保存为成员变量,供后续 PreCollectGarbageImpl / CollectGarbageImpl / PostCollectGarbageImpl 使用。
  3. bFullPurge=trueGAllowIncrementalReachability=false → 不使用时间限制(一次性完成)。否则启用增量模式,每帧处理一个时间片。

PerformReachabilityAnalysisAndConditionallyPurgeGarbage

这个函数主要三个流程

void FReachabilityAnalysisState::PerformReachabilityAnalysisAndConditionallyPurgeGarbage(
    bool bReachabilityUsingTimeLimit)
{
    LLM_SCOPE(ELLMTag::GC);

    // [1] 统计初始化(仅新轮次首次迭代)
    if (!GIsIncrementalReachabilityPending)
    {
        GGCStats = FStats();
        GGCStats.NumObjects = GUObjectArray.GetObjectArrayNumMinusAvailable() - GUObjectArray.GetFirstGCIndex();
        GGCStats.NumClusters = GUObjectClusters.GetNumAllocatedClusters();
    }

    // [2] Verse VM:释放 Verse 堆访问权,避免与 GC 同时操作内存
    #if WITH_VERSE_VM
    Verse::FIOContext Context = RelinquishVerseHeapAccess(RunningContext);
    #endif

    // [3] 预处理阶段
    PreCollectGarbageImpl<bPerformFullPurge>(ObjectKeepFlags);

    // [4] 决定是否强制非增量
    const bool bForceNonIncrementalReachability =
        !GIsIncrementalReachabilityPending &&
        (bPerformFullPurge || !GAllowIncrementalReachability);

    // [5] 执行可达性分析
    PerformReachabilityAnalysis();
    GIsIncrementalReachabilityPending = GReachabilityState.IsSuspended();

    // [6] 垃圾引用追踪重跑
    if (!GIsIncrementalReachabilityPending && Stats.bFoundGarbageRef && GGarbageReferenceTrackingEnabled > 0)
    {
        FRealtimeGC GC;
        GC.Stats = Stats;
        GC.PerformReachabilityAnalysis(ObjectKeepFlags, GetReferenceCollectorOptions(bPerformFullPurge));
    }

    // [7] 后处理阶段
    PostCollectGarbageImpl<bPerformFullPurge>(ObjectKeepFlags);

    // [8] 恢复 Verse 堆访问
    #if WITH_VERSE_VM
    AcquireVerseHeapAccess(Context);
    #endif
}

Verse不看,是虚幻那个编程语言,但是没推广开吧。

第6点不看,这个是分析GC流程用的。在第5点里面 PerformReachabilityAnalysis 本质上也是调用到 FRealtimeGC 的 PerformReachabilityAnalysis

总共就三个流程,我先简单概括一下

  • PreCollectGarbageImpl
    • 预先的处理,如果之前有时间分片挂起的GC要完成,避免干扰。维护出一个InitObject的根列表。这些肯定是不GC的,后面顺着这个去标记其他可达对象。
  • PerformReachabilityAnalysis
    • 具体的可达性分析对象,核心逻辑也会走到 FRealtimeGC 里面的 PerformReachabilityAnalysis
  • PostCollectGarbageImpl
    • 具体对不可达的对象做清理的地方。

PreCollectGarbageImpl

就叫他GC预处理吧,用true,false 区别是不是全量

/** 
 * Deletes all unreferenced objects, keeping objects that have any of the passed in KeepFlags set
 *
 * @param   KeepFlags           objects with those flags will be kept regardless of being referenced or not
 * @param   bPerformFullPurge   if true, perform a full purge after the mark pass
 */
template<bool bPerformFullPurge>
void PreCollectGarbageImpl(EObjectFlags KeepFlags)
{
    // GC 已获得控制权,重置"正在等待"标记
    FGCCSyncObject::Get().ResetGCIsWaiting();

    // 加载期间 Import 引用未建立,此时 GC 会产生错误的可达性结果
    check(!IsLoading());

    // 重置 GC 跳过计数器。(TryCollectGarbage 获锁失败时递增,超过 gc.NumRetriesBeforeForcingGC 次强制 GC)
    GNumAttemptsSinceLastGC = 0;

    // 非增量GC,仅在新轮次首次迭代执行
    if (!GIsIncrementalReachabilityPending)
    {
        // 先释放 GC 锁,后面要调用用户代码和异步逻辑,持有锁容易出问题
        ReleaseGCLock();

        // 若 CVar gc.FlushStreamingOnGC 开启,强制完成所有异步加载,确保对象引用关系稳定
        if (GFlushStreamingOnGC && IsAsyncLoading())
            FlushAsyncLoading();

        // 广播预 GC 委托,外部系统做准备(如释放临时强引用)
        FCoreUObjectDelegates::GetPreGarbageCollectDelegate().Broadcast();

        // 防御性设计,上面的委托可能触发了新的异步加载,再刷一遍
        if (GFlushStreamingOnGC && IsAsyncLoading())
            FlushAsyncLoading();

        // 重新获取 GC 独占锁,进入临界区
        AcquireGCLock();
    }

    GIsGarbageCollecting = true; // 设置全局标记,已经开始GC了
    if (!GIsIncrementalReachabilityPending)
        FCoreUObjectDelegates::GetGarbageCollectStartedDelegate().Broadcast(); // 广播 GC 已开始委托

    // 如果上轮 GC 标记的不可达对象还没清除完(增量销毁仍在进行),必须在新一轮标记前【强制完成】
    // 否则旧的 `RF_Unreachable` 标记会干扰本轮可达性分析。`false` = 不使用时间限制
    if (IsIncrementalPurgePending())
    {
        IncrementalPurgeGarbage(false);
        if (!bPerformFullPurge) FMemory::Trim();
    }

    // 锁定 UObject 哈希表,分析期间禁止 `NewObject()` / `StaticFindObject()` / 重命名等操作
    GIsGarbageCollectingAndLockingUObjectHashTables = true;
    LockUObjectHashTables();

    // 断言 + 集群溶解 + 校验
    if (!GIsIncrementalReachabilityPending)
    {
        check(!GObjIncrementalPurgeIsInProgress);
        check(!GObjPurgeIsRequired); // 断言确认上轮清除已完成
        if (!GCreateGCClusters && GUObjectClusters.GetNumAllocatedClusters())
            GUObjectClusters.DissolveClusters(true);  // 若运行时禁用了集群(`gc.CreateGCClusters=0`)但有存量集群→溶解

        // 调试构建中验证三项假设
        // - DisregardForGC 池对象不引用可 GC 对象
        // - 集群引用一致性
        // - 对象标记无矛盾
        #if VERIFY_DISREGARD_GC_ASSUMPTIONS
        VerifyGCAssumptions();
        VerifyClustersAssumptions();
        VerifyObjectFlags();
        #endif
    }
}

尝试总结一下,预处理阶段

  • 做了一些清理和重置,例如运行GC时候有增量的GC未完成要强制他完成。
  • 锁UObject哈希表,不允许GC的时候 NewObject, FindStaticObject, 重命名操作。Post的时候才解开。(FUObjectHashTables::Get().Lock();)

CollectGarbageImpl

标记阶段。

void FReachabilityAnalysisState::PerformReachabilityAnalysisAndConditionallyPurgeGarbage(
    bool bReachabilityUsingTimeLimit)
{
    // ...    

    // [3] 预处理阶段
    PreCollectGarbageImpl<bPerformFullPurge>(ObjectKeepFlags);

    // [5] 执行可达性分析
    PerformReachabilityAnalysis();

    // [7] 后处理阶段
    PostCollectGarbageImpl<bPerformFullPurge>(ObjectKeepFlags);

    // ...
}

我们要看这里的可达性分析

这个 PerformReachabilityAnalysis

void FReachabilityAnalysisState::PerformReachabilityAnalysis()
{
    if (!bIsSuspended) // 全新开始(非恢复)
    {
        Init(); // 初始化标记状态,里面只是一个赋值
        NumRechabilityIterationsToSkip = FMath::Max(0, GDelayReachabilityIterations);
    }

    // → CollectGarbageImpl<true>
    if (bPerformFullPurge)
    {
        UE::GC::CollectGarbageFull(ObjectKeepFlags);
    }
    // → CollectGarbageImpl<false>
    else if (NumRechabilityIterationsToSkip == 0 || // Delay reachability analysis by NumRechabilityIterationsToSkip (if desired)
        !bIsSuspended || // but only but only after the first iteration (which also does MarkObjectsAsUnreachable)
        IterationTimeLimit <= 0.0f) // and only when using time limit (we're not using the limit when we're flushing reachability analysis when starting a new one or on exit)
    {
        UE::GC::CollectGarbageIncremental(ObjectKeepFlags);
    }
    else
    {
        --NumRechabilityIterationsToSkip; // 跳过本次迭代(延迟启动优化)
    }

    FinishIteration(); // 更新迭代状态,可能设置 bIsSuspended
}
  1. Full Purge → CollectGarbageFull → 不限时完成所有标记
  2. 增量模式且不跳过 → CollectGarbageIncremental → 受时间片限制,可能挂起
  3. 跳过 → GDelayReachabilityIterations CVar 控制,允许延迟几帧再开始真正的标记遍历(给其他系统喘息时间)

下一步会到这里,用ture false 区别是否全量

CollectGarbageImpl 和 CollectGarbageImpl,到一起了。

template<bool bPerformFullPurge>
void CollectGarbageImpl(EObjectFlags KeepFlags)
{
    {
        // Reachability analysis.
        {
            const EGCOptions Options = GetReferenceCollectorOptions(bPerformFullPurge);

            // Perform reachability analysis.
            FRealtimeGC GC;
            GC.PerformReachabilityAnalysis(KeepFlags, Options);
        }
    }
}

创建 FRealtimeGC 实例,根据 Options 选择:

  • 并行 / 单线程(EGCOptions::Parallel)
  • 是否启用垃圾消除(EGCOptions::EliminateGarbage)
  • 是否增量可达性(EGCOptions::IncrementalReachability)

FRealtimeGC::PerformReachabilityAnalysis

// GarbageCollection.cpp:4466
void PerformReachabilityAnalysis(EObjectFlags KeepFlags, const EGCOptions Options)
{
    StartReachabilityAnalysis(KeepFlags, Options);
    StartVerseGC();  // UE 5.4+ Verse 集成

    while (true)
    {
        PerformReachabilityAnalysisPass(Options);  // 多线程引用遍历
        if (GReachabilityState.IsSuspended())       // 增量模式:可挂起
        {
            if (EnumHasAnyFlags(Options, EGCOptions::IncrementalReachability))
                break;
        }
        else if (Private::GReachableObjects.IsEmpty() /* ... */)
        {
            StopVerseGC();
            break;  // 标记完成
        }
    }
}

verse不看。

主要是两块:

  • StartReachabilityAnalysis 构建了一个可达性查询的根列表。TArray<UObject*> InitialObjects;
  • PerformReachabilityAnalysisPass 多线程引用遍历。

StartReachabilityAnalysis

在首次进入可达性分析时(非从增量挂起恢复),执行初始化:

压缩一下

void StartReachabilityAnalysis(EObjectFlags KeepFlags, const EGCOptions Options)
{
    BeginInitialReferenceCollection(Options); // [1] 后台收集 FGCObject 引用

    GObjectCountDuringLastMarkPhase.Reset();
    InitialObjects.Reset();

    // [2] cooked 平台特殊处理
    if (FPlatformProperties::RequiresCookedData() && GUObjectArray.IsDisregardForGC(FGCObject::GGCObjectReferencer))
        InitialObjects.Add(FGCObject::GGCObjectReferencer);

    // [3] 标记所有对象为 MaybeUnreachable + 收集根集合
    MarkObjectsAsUnreachable(KeepFlags);
}

【1】BeginInitialReferenceCollection 并行模式下在后台 Task 中提前调用 【FGCObject::GGCObjectReferencer->AddInitialReferences(InitialReferences)】收集所有FGCObject(非 UObject 的 C++ 对象,如 Subsystem 等)持有的 UObject 引用。主线程可同时执行[3] 的标记工作

  • 这个就是继承 FGCObject 然后 AddReferencedObjects 的那种不被GC的那种方式
  • 这可以是异步的(UE::Tasks::Launch)

【2】GGCObjectReferencer 通常在 DisregardForGC 池中(GC 跳过的永久对象),但它引用了所有 FGCObject 注册的对象,必须被扫描。在 cooked 平台上特殊添加到初始对象集

  • 和【1】乍一看好像呀,其实这个只是把 GGCObjectReferencer 这个UObject加入根集合。别漏了

【3】MarkObjectsAsUnreachable 标记完成后,InitialObjects 就有了完整的根集合。

  • 有好几步,来一个大标题

MarkObjectsAsUnreachable

MarkObjectsAsUnreachable_翻转

// GarbageCollection.cpp:4313
FORCENOINLINE void MarkObjectsAsUnreachable(const EObjectFlags KeepFlags)
{
    // 原子翻转,所有对象 → MaybeUnreachable
    if (const bool bInitialMark = !Stats.bFoundGarbageRef)
    {
        FGCFlags::SwapReachableAndMaybeUnreachable();
    }
    else
    {
        // 。。。
    }

    // 簇根和根对象立即重标记为 Reachable
    MarkClusteredObjectsAsReachable(GatherOptions, InitialObjects);
    MarkRootObjectsAsReachable(GatherOptions, KeepFlags, InitialObjects);
}

第一步是反转枚举的语义。

    /** Current EInternalObjectFlags value representing a reachable object */
    EInternalObjectFlags FGCFlags::ReachableObjectFlag = EInternalObjectFlags::ReachabilityFlag0;

    /** Current EInternalObjectFlags value representing a maybe unreachable object */
    EInternalObjectFlags FGCFlags::MaybeUnreachableObjectFlag = EInternalObjectFlags::ReachabilityFlag1;
Swap(ReachableObjectFlag, MaybeUnreachableObjectFlag);

由于前面pre阶段,对增量GC的要全部完成,所以不可达的对象都已经没了。

剩下的都是可达的对象。直接改变枚举的语义。O(1) 状态下完成全部标记为不可达。

这个 if (const bool bInitialMark = !Stats.bFoundGarbageRef) 什么时候是改变?

99%都是跑上面的翻转的,特殊情况,垃圾引用追踪重跑(else里面的逻辑)

在非 Shipping 的调试构建中,如果开启了 CVar gc.GarbageReferenceTrackingEnabled,GC 的第一轮可达性分析完成后可能会发现:

  • 某个已被标记为垃圾(Garbage)的对象,仍然被一个可达对象引用着。这属于逻辑错误(悬挂引用/泄漏),

此时引擎会设置 Stats.bFoundGarbageRef = true

在后面的流程中会用 TDebugReachabilityProcessor 进行额外的记录。这里刚刚跑完一轮,不满足那个pre阶段不可达对象都没了。所以只能一个一个检查。

====

接下来是这两个函数

    MarkClusteredObjectsAsReachable(GatherOptions, InitialObjects);
    MarkRootObjectsAsReachable(GatherOptions, KeepFlags, InitialObjects);

MarkObjectsAsUnreachable_簇标记

MarkClusteredObjectsAsReachable 簇对象标记

对所有对象簇进行并行遍历,决策保留或溶解:

对象簇的概念我们上面已经聊过了。

另外ParallelFor是多线程的写法,会阻塞在这里,全部完成才会下去

// Num = 总工作量,引擎自动把 1000 个元素分配到多个线程
ParallelFor(1000, [&](int32 Index)
{
    ProcessItem(Index);  // 每次调用处理 1 个元素
});

代码做了一定简化,去除了一下合法性校验,保留关键代码。

ParallelFor(TEXT("GC.MarkClusteredObjectsAsReachable"), NumWorkerThreads, 1,
    [&ThreadIterators, &ClusterArray, &TotalClusteredObjects](int32 ThreadIndex)
{
    while (ThreadState.Index <= ThreadState.LastIndex)
    {
        FUObjectCluster& Cluster = ClusterArray[ClusterIndex];
        FUObjectItem* RootItem = &GUObjectArray.GetObjectItemArrayUnsafe()[Cluster.RootIndex];

        if (!RootItem->IsGarbage())
        {
            bool bKeepCluster = RootItem->HasAnyFlags(EInternalObjectFlags_RootFlags);
            if (bKeepCluster)
                FGCFlags::FastMarkAsReachableInterlocked_ForGC(RootItem); // 根标记为可达

            for (int32 ObjectIndex : Cluster.Objects)
            {
                FUObjectItem* ClusteredItem = &GUObjectArray.GetObjectItemArrayUnsafe()[ObjectIndex];
                FGCFlags::FastMarkAsReachableAndClearReachableInClusterInterlocked_ForGC(ClusteredItem);
                if (!bKeepCluster && ClusteredItem->HasAnyFlags(EInternalObjectFlags_RootFlags))
                {
                    ThreadState.Payload.KeepClusters.Add(RootItem);
                    bKeepCluster = true; // 成员是根 → 整个簇保留
                }
            }
        }
        else
        {
            ThreadState.Payload.ClustersToDissolve.Add(RootItem); // 根是垃圾 → 溶解簇
        }
    }
});

先不纠结一下多线程。ParallelFor 可以理解为一个任务池,多个线程做完就抢下一个。

int32 ClusterIndex = ThreadState.Index++; 这里的 ClusterIndex 就是抢到了,要开始做了。

FUObjectItem* RootItem = &GUObjectArray.GetObjectItemArrayUnsafe()[Cluster.RootIndex];

这个我们前面也提到过,UObject在全局Array的元数据,里面是这些东西。

struct FUObjectItem
{
    UObjectBase* Object;     // 指向 UObject 的指针
    int32 Flags;             // EInternalObjectFlags(Garbage、Reachable、ClusterRoot 等)
    int32 ClusterRootIndex;  // 簇索引(负值)或 Owner 索引(正值)
    int32 SerialNumber;      // TWeakObjectPtr 用的序列号,防止悬空引用
    int32 RefCount;          // 引用计数,>0 时阻止 GC 回收
};

RootItem->IsGarbage() 就是检查这些标记位,标记位上面时候被标记呢?例如一个Actor销毁后,被逻辑标记为垃圾。

MarkAsGarbage, 以前叫 PendingKill

if (!RootItem->IsGarbage())
{
    bool bKeepCluster = RootItem->HasAnyFlags(EInternalObjectFlags_RootFlags);
    if (bKeepCluster)
        FGCFlags::FastMarkAsReachableInterlocked_ForGC(RootItem); // 根标记为可达

    for (int32 ObjectIndex : Cluster.Objects)
    {
        FUObjectItem* ClusteredItem = &GUObjectArray.GetObjectItemArrayUnsafe()[ObjectIndex];
        FGCFlags::FastMarkAsReachableAndClearReachableInClusterInterlocked_ForGC(ClusteredItem);
        if (!bKeepCluster && ClusteredItem->HasAnyFlags(EInternalObjectFlags_RootFlags))
        {
            ThreadState.Payload.KeepClusters.Add(RootItem);
            bKeepCluster = true; // 成员是根 → 整个簇保留
        }
    }
}
else
{
    ThreadState.Payload.ClustersToDissolve.Add(RootItem); // 根是垃圾 → 溶解簇
}

判断后两个分支

  • 簇根不是垃圾
    • 标记根和所有成员为 Reachable。如果簇根或任何成员具有根标志(RootSetNative 等)→ 整个簇保留并加入 KeepClusters
  • 簇根是垃圾
    • 加入 ClustersToDissolve,后续调用 GUObjectClusters.DissolveClusterAndMarkObjectsAsUnreachable() 溶解簇,成员对象单独参与后续 GC

保留的簇根后续调用 MarkReferencedClustersAsReachable() 递归标记所有被引用的簇

===

在这个多线程任务返回后。合并线程结果。(目前我们先纠结GC流程,这里面应该还有一些多线程的写法值得探讨)

【合并线程结果】

FMarkClustersArrays MarkClustersResults;
GatherClustersState.Finish(MarkClustersResults);

Finish() 将所有线程的 KeepClustersClustersToDissolve 拼接到一个结果中(单线程时直接 Move,多线程时先 Reserve 再逐线程 Append)。

【溶解垃圾簇 + 传播可达性】

// 溶解垃圾簇,成员对象恢复为独立对象参与后续 GC
for (FUObjectItem* ObjectItem : MarkClustersResults.ClustersToDissolve)
{
    // 检查该对象是否仍为集群根节点 —— 有可能是之前其中一个
    // DissolveClusterAndMarkObjectsAsUnreachable calls already dissolved its cluster
    if (ObjectItem->HasAnyFlags(EInternalObjectFlags::ClusterRoot))
    {
        GUObjectClusters.DissolveClusterAndMarkObjectsAsUnreachable(ObjectItem);
        GUObjectClusters.SetClustersNeedDissolving();
    }
}

// 递归传播可达性
for (FUObjectItem* ObjectItem : MarkClustersResults.KeepClusters)
{
    checkSlow(ObjectItem->HasAnyFlags(EInternalObjectFlags::ClusterRoot));
    // this thing is definitely not marked unreachable, so don't test it here
    // Make sure all referenced clusters are marked as reachable too
    MarkReferencedClustersAsReachable<EGCOptions::None>(ObjectItem->GetClusterIndex(), OutRootObjects);
}

挺复杂的。。。都是总体来说是这样。最后拿到了一个 InitialObjects

MarkClusteredObjectsAsReachable()
  │
  ├─ [并行] 对每个簇分类:
  │    ├─ 簇根 NOT Garbage + 有 RootFlags → KeepClusters(标记根+成员 Reachable)
  │    ├─ 簇根 NOT Garbage + 无 RootFlags 但成员有 → KeepClusters
  │    ├─ 簇根 NOT Garbage + 均无 RootFlags → 仅标记 Reachable,不加入 Keep
  │    └─ 簇根 IS Garbage → ClustersToDissolve
  │
  ├─ [主线程] 合并各线程的 KeepClusters / ClustersToDissolve
  │
  ├─ [主线程] 溶解垃圾簇 → 成员变为独立对象参与后续 GC
  │
  └─ [主线程] 对保留簇调用 MarkReferencedClustersAsReachable()
       ├─ 递归标记被引用簇根为 Reachable
       ├─ 标记簇内可变对象为 Reachable
       └─ 若发现被引用簇根已是 Garbage → 清空引用 + 标记当前簇需溶解

MarkObjectsAsUnreachable_根对象可达

MarkRootObjectsAsReachable(GatherOptions, KeepFlags, InitialObjects);

【显式根对象(GRoots)】

GRootsCritical.Lock();
TArray<int32> RootsArray(GRoots.Array());
GRootsCritical.Unlock();

ParallelFor(TEXT("GC.MarkRootObjectsAsReachable"), NumWorkerThreads, 1,
    [&](int32 ThreadIndex)
{
    while (ThreadState.Index <= ThreadState.LastIndex)
    {
        FUObjectItem* RootItem = &GUObjectArray.GetObjectItemArrayUnsafe()[RootsArray[ThreadState.Index++]];
        UObject* Object = static_cast<UObject*>(RootItem->GetObject());
        FGCFlags::FastMarkAsReachableInterlocked_ForGC(RootItem);
        ThreadState.Payload.Add(Object); // 加入初始对象集
    }
});

GRoots 存储通过 UObject::AddToRoot() 注册的对象索引。加锁拷贝数组后并行处理。

【带 KeepFlags 的对象】

if (KeepFlags != RF_NoFlags)
{
    ParallelFor(TEXT("GC.SlowMarkObjectAsReachable"), NumWorkerThreads, 1,
        [&](int32 ThreadIndex)
    {
        while (ThreadState.Index <= ThreadState.LastIndex)
        {
            FUObjectItem* ObjectItem = &GUObjectArray.GetObjectItemArrayUnsafe()[ThreadState.Index++];
            UObject* Object = static_cast<UObject*>(ObjectItem->GetObject());
            if (Object &&
                !ObjectItem->HasAnyFlags(EInternalObjectFlags_RootFlags) && // 已在第一步处理
                !(bWithGarbageElimination && ObjectItem->IsGarbage()) &&
                Object->HasAnyFlags(KeepFlags))
            {
                FGCFlags::FastMarkAsReachableInterlocked_ForGC(ObjectItem);
                ThreadState.Payload.Add(Object);
            }
        }
    });
}

遍历所有 GC 对象(很慢)检查 EObjectFlags

通常 KeepFlags 在编辑器中是 RF_Standalone

总结:

两步结果合并后,InitialObjects 包含:簇根对象 + 显式根对象 + KeepFlags 对象 = 完整的根集合。GGCStats.NumRoots 记录根集合大小。

PerformReachabilityAnalysisPass

前面一堆是StartReachabilityAnalysis里面的流程,拿到了 InitialObjects

void PerformReachabilityAnalysis(EObjectFlags KeepFlags, const EGCOptions Options)
{
    if (!GReachabilityState.IsSuspended())        // 仅全新开始时
    {
        StartReachabilityAnalysis(KeepFlags, Options);
        StartVerseGC();                            // Verse VM 同步启动 GC
    }

    while (true)
    {
        PerformReachabilityAnalysisPass(Options);  // 执行一轮遍历

        if (GReachabilityState.IsSuspended())      // 时间片用完或 Verse 仍在标记
        {
            if (EnumHasAnyFlags(Options, EGCOptions::IncrementalReachability))
                break;                              // 增量模式:挂起退出,下帧继续
            // 非增量模式但被挂起(Verse GC 仍在标记)→ 继续循环
        }
        else if (Private::GReachableObjects.IsEmpty()
              && Private::GReachableClusters.IsEmpty()
              /* && Verse structs empty */)
        {
            StopVerseGC();
            break;                                  // 所有工作完成,退出
        }
        // 否则:GC Write Barrier 或 Verse 发现了新的可达对象 → 继续循环
    }
}

接下来主要是 while true 和 PerformReachabilityAnalysisPass 里面的流程了。

循环反复执行 Pass 直到没有更多新可达对象被发现。退出条件有两种:

  1. 增量挂起:时间片用完,IsSuspended() 返回 true,增量模式下 break 退出
  2. 正常完成:未挂起且所有并发队列为空(GReachableObjectsGReachableClusters

PerformReachabilityAnalysisPass 里面的流程

void PerformReachabilityAnalysisPass(const EGCOptions Options)
{
    FContextPoolScope Pool;
    FWorkerContext* Context;

    if (!GReachabilityState.IsSuspended())
        Context = Pool.AllocateFromPool();         // 全新 Pass:从池分配工作上下文
    else
    {
        Context = GReachabilityState.GetContextArray()[0]; // 恢复:使用挂起的上下文
        Context->bDidWork = false;
        InitialObjects.Reset();
    }

    // [A] 处理 GC Write Barrier 新发现的对象
    if (!Private::GReachableObjects.IsEmpty())
    {
        Private::GReachableObjects.PopAllAndEmpty(InitialObjects);
        GGCStats.NumBarrierObjects += InitialObjects.Num();
    }
    else if (GReachabilityState.GetNumIterations() == 0 || ...)
    {
        Context->InitialNativeReferences = GetInitialReferences(Options); // 首次:获取 FGCObject 引用
    }

    // [B] 处理 GC Write Barrier 新发现的簇
    if (!Private::GReachableClusters.IsEmpty())
    {
        TArray<FUObjectItem*> KeepClusterRefs;
        Private::GReachableClusters.PopAllAndEmpty(KeepClusterRefs);
        for (FUObjectItem* ObjectItem : KeepClusterRefs)
            MarkReferencedClustersAsReachable<EGCOptions::None>(ObjectItem->GetClusterIndex(), InitialObjects);
    }

    // [C] 设置初始对象并执行引用遍历
    Context->SetInitialObjectsUnpadded(InitialObjects);
    PerformReachabilityAnalysisOnObjects(Context, Options);

    // [D] 完成后更新统计
    if (!GReachabilityState.IsSuspended())
    {
        GReachabilityState.ResetWorkers();
        Stats.AddStats(Context->Stats);
        Pool.ReturnToPool(Context);
    }
}

FWorkerContext 是每个工作线程的私有上下文,包含:待处理对象队列(ObjectsToSerialize)、发现的弱引用列表、垃圾引用记录、GC 历史记录等。

======

    // [A] 处理 GC Write Barrier 新发现的对象

    // Private::GReachableObjects 是一个无锁并发栈。它的作用是:当增量 GC 正在进行标记时,游戏线程仍然在运行,
    // 可能修改 UObject 的引用关系(比如 ObjectA->Ref = ObjectB)。这时游戏线程通过 Write Barrier(写屏障)
    // 把 ObjectB 推入这个栈,告诉 GC:"我刚让一个对象变得可达了,你别漏掉它。"
    if (!Private::GReachableObjects.IsEmpty())
    {
        // 一次性取出所有这些新发现的对象,放进 InitialObjects。这些对象将作为本轮 Pass 的起点,沿着它们的引用继续往下遍历。
        Private::GReachableObjects.PopAllAndEmpty(InitialObjects);
        GGCStats.NumBarrierObjects += InitialObjects.Num();
    }
    else if (GReachabilityState.GetNumIterations() == 0 || ...)
    {
        // 如果没有 Write Barrier 对象,且这是第一次迭代(就是整个 GC 刚开始的首轮 Pass)
        // 那就去调用 GetInitialReferences(Options)。
        // 等待之前 Task 中启动的 FGCObject 引用收集完成(对应 BeginInitialReferenceCollection 那一步)
        Context->InitialNativeReferences = GetInitialReferences(Options); // 首次:获取 FGCObject 引用
    }

这里else if 有点怪。是说首轮GC的 GReachableObjects 一定是空的,走下面的流程。

    // [B] 处理 GC Write Barrier 新发现的簇
    if (!Private::GReachableClusters.IsEmpty())
    {
        TArray<FUObjectItem*> KeepClusterRefs;
        Private::GReachableClusters.PopAllAndEmpty(KeepClusterRefs);
        for (FUObjectItem* ObjectItem : KeepClusterRefs)
            MarkReferencedClustersAsReachable<EGCOptions::None>(ObjectItem->GetClusterIndex(), InitialObjects);
    }

感觉是一样的,之前前一段的UObject这里变成了“簇”

// Private::GReachableObjects 是一个无锁并发栈。它的作用是:当增量 GC 正在进行标记时,游戏线程仍然在运行,
// 可能修改 UObject 的引用关系(比如 ObjectA->Ref = ObjectB)。这时游戏线程通过 Write Barrier(写屏障)
// 把 ObjectB 推入这个栈,告诉 GC:"我刚让一个对象变得可达了,你别漏掉它。"

======

// [C] 设置初始对象并执行引用遍历
Context->SetInitialObjectsUnpadded(InitialObjects);
PerformReachabilityAnalysisOnObjects(Context, Options);

SetInitialObjectsUnpadded

【cache miss 问题】

CPU 访问内存时,真正读写的不是主存(RAM),而是缓存(L1/L2/L3 Cache)。缓存比主存快几十到几百倍。当 CPU 需要的数据不在缓存中时,就发生 cache miss,CPU 必须等待数据从主存加载到缓存——这个等待通常是 100~300 个时钟周期,期间 CPU 基本在空转。

GC 遍历对象引用时,对每个对象都需要访问:

UObject* Obj → Obj->GetClass()     → Class 内存(另一个地址)
                 → Class->ReferenceSchema  → Schema 内存(又一个地址)
               → Obj->GetOuter()   → Outer 对象内存(又一个地址)

这些数据分散在堆内存的不同位置。UE 的 GC 可能有几十万个对象要遍历,每个对象的 Class、Schema、Outer 几乎不可能恰好在缓存里。所以每处理一个对象就要等 3~4 次 cache miss,效率很低。

预取(Prefetch)怎么解决

CPU 有一条特殊指令 prefetch:提前告诉 CPU "我一会要用这个地址的数据,你现在就开始从主存搬到缓存"。CPU 收到指令后在后台搬运,不会阻塞当前执行。

FPrefetchingObjectIterator 的策略是——处理当前对象时,提前告诉 CPU 去准备后面第 2、6、16 个对象的数据:

// class FPrefetchingObjectIterator
void Advance()
{
    // 我正在处理 It[0],但同时:
    Prefetch(It[2]->GetClass()->ReferenceSchema);   // 预取第 2 个的 Schema 数据
    PrefetchOuter(It[6]);                             // 预取第 6 个的 Outer 对象
    Prefetch(It[6]->GetClass(), offsetof(UClass, ReferenceSchema)); // 预取第 6 个的 Class→Schema 指针
    PrefetchClass(It[16]);                            // 预取第 16 个的 Class 对象
    ++It;
}

回到 SetInitialObjectsUnpadded

  • 填充尾部(Pad)
    • GC 遍历时使用 FPrefetchingObjectIterator,会提前 16 个对象预取内存(预取 Class、Schema、Outer 等),避免 cache miss 造成的延迟:
    • 这里填充是为了别越界
  • 存入 Context->InitialObjects

=======

PerformReachabilityAnalysisOnObjects(Context, Options);

这里根据 EGCOptions 的枚举,重定向到不同的函数,当然是同一个函数,只是基于模板填了不同的参数

virtual void PerformReachabilityAnalysisOnObjects(FWorkerContext* Context, EGCOptions Options) override
{
    (this->*ReachabilityAnalysisFunctions[GetGCFunctionIndex(Options)])(*Context);
}

然后到了 PerformReachabilityAnalysisOnObjectsInternal 这里。

template <EGCOptions Options>
void PerformReachabilityAnalysisOnObjectsInternal(FWorkerContext& Context)
{
#if !UE_BUILD_SHIPPING
    // 调试模式:使用 TDebugReachabilityProcessor 记录引用历史/追踪垃圾
    TDebugReachabilityProcessor<Options> DebugProcessor;
    if (DebugProcessor.IsForceEnabled() | DebugProcessor.TracksHistory() | ...)
    {
        CollectReferencesForGC<TDebugReachabilityCollector<Options>>(DebugProcessor, Context);
        return;
    }
#endif
    // 正常路径
    TReachabilityProcessor<Options> Processor;
    CollectReferencesForGC<TReachabilityCollector<Options>>(Processor, Context);
}

TReachabilityProcessor 处理每个发现的引用:若目标对象是 MaybeUnreachable → 原子地标记为 Reachable → 加入工作队列继续遍历。

TReachabilityCollectorFReferenceCollector 的模板特化,配合 TFastReferenceCollector 使用预计算的 Schema(Token Stream)高效遍历 UPROPERTY 引用。

Schema是什么?

Schema 是 UE 为每个 UClass 预编译的一张"引用地图"——它记录了这个类的实例中,哪些字节偏移处存放着 UObject 引用,以及引用的类型(单指针、数组、结构体数组等)。在UClass 初始化的时候构建。struct FMemberPacked 结构叫这个,回头看看。

没有这个,只能 AddReferencedObjects 里面告诉GC。

CollectReferencesForGC

单线程和多线程的遍历了

template<class CollectorType, class ProcessorType>
void CollectReferencesForGC(ProcessorType& Processor, FWorkerContext& Context)
{
    using FastReferenceCollector = TFastReferenceCollector<ProcessorType, CollectorType>;

    if constexpr (IsParallel(ProcessorType::Options))
    {
        ProcessAsync(/*...*/, &Processor, Context);     // 多线程路径
    }
    else
    {
        GReachabilityState.SetupWorkers(1);
        FastReferenceCollector(Processor).ProcessObjectArray(Context);  // 单线程路径
        GReachabilityState.CheckIfAnyContextIsSuspended();
    }
}

TFastReferenceCollector::ProcessObjectArray

是引用遍历的最内层核心,执行 BFS:

  1. Context.ObjectsToSerialize 队列取出对象(使用 FPrefetchingObjectIterator 提前 16 个对象预取)
  2. 通过 UClass 的 GC Token Stream(FSchemaView)遍历所有 UPROPERTY 声明的引用——这是编译期预计算的引用偏移/类型布局,无虚函数开销(虚函数开销指子类需要 AddReferencedObjects)
  3. 对有自定义引用的类调用 AddReferencedObjects()——Schema 无法捕获的引用模式
  4. 每个被引用对象若仍是 MaybeUnreachable → 原子标记为 Reachable → 入队继续遍历
  5. 增量模式下定期调用 IsTimeLimitExceeded(),超时则设置 bIsSuspended = true 退出

ProcessAsync — 多线程

void ProcessAsync(void (*ProcessSync)(void*, FWorkerContext&), void* Processor, FWorkerContext& InContext)
{
    // 将初始对象均匀分配到 N 个 `FWorkerContext`,每个 Context 有独立的工作队列
    TArrayView<FWorkerContext*> Contexts = InitializeAsyncProcessingContexts(InContext);

    // 协调工作窃取。当某线程的队列为空时,可从其他线程的队列窃取工作
    TSharedRef<FWorkCoordinator> WorkCoordinator = MakeShared<FWorkCoordinator>(Contexts, NumWorkerThreads);

    // 启动 N-1 个后台 Task
    for (int32 Idx = 1; Idx < GReachabilityState.GetNumWorkers(); ++Idx)
    {
        AllWorkerTasks.Add(Tasks::Launch(TEXT("CollectReferences"), [=]()
        {
            if (FWorkerContext* Context = WorkCoordinator->TryStartWorking(Idx))
                ProcessSync(Processor, *Context);
        }));
    }

    // 主线程自己也参与工作,自己也作为 Worker 0
    if (FWorkerContext* Context = WorkCoordinator->TryStartWorking(0))
        ProcessSync(Processor, *Context);

    // 等待所有工作完成(可窃取其他线程的工作)《= 阻塞
    WorkCoordinator->SpinUntilAllStopped();

    // 处理挂起的上下文(增量模式可能有未完成的工作)
    for (FWorkerContext* Context : Contexts)
    {
        if (Context->ObjectsToSerialize.HasWork() && !Context->bDidWork)
        {
            if (GReachabilityState.IsTimeLimitExceeded())
                Context->bIsSuspended = true;    // 时间用完,标记挂起
            else
            {
                // 罕见情况:工作被丢弃(增量模式不支持上下文窃取),重新处理
                ProcessSync(Processor, *Context);
            }
        }
    }
}

增量超时处理:

时间片用完时,未完成工作的上下文标记为挂起(bIsSuspended = true),下次 PerformReachabilityAnalysisPass 从这些上下文恢复

IsTimeLimitExceeded 增量的判断接口。

这里 ProcessSync 是输入参数进来的

[](void* P, FWorkerContext& C) { FastReferenceCollector(*reinterpret_cast<ProcessorType*>(P)).ProcessObjectArray(C); }

这个多线程BFS是怎么回事?

。。。下一篇把,截断

标记总结

UE 的可达性分析(PerformReachabilityAnalysis)是一个典型的三色标记 GC 变体,核心思路是"从根出发,沿引用链传播可达性,遍历结束后仍不可达的对象即为垃圾"。

具体分为两大阶段:

首先通过 O(1) 的标志位语义交换将所有对象瞬间标记为"可能不可达",

然后从根集合(显式 AddToRoot 对象、带根标志的原生/异步加载对象、编辑器下带 RF_Standalone 的资产、以及簇根)出发,

利用每个 UClass 预计算的 Schema 偏移表高效遍历实例中的所有 UObject 引用,将可达对象重新标记为"可达"。

遍历采用多线程工作窃取模型(最多 16 线程),配合多级预取策略(提前 2/6/16 个对象分层预取 Schema、Outer、Class 数据)隐藏内存延迟(避免内存cache miss 那一个操作)。对于 Schema 无法覆盖的自定义引用模式,回退到 AddReferencedObjects 虚函数慢路径(一般也不用这个吧?就是子类重写这个函数,然后告诉UE的系统);非 UObject 的 C++ 对象则通过 FGCObjectUGCObjectReferencer 桥接纳入遍历。

整个过程支持增量模式——按时间片挂起/恢复,避免单帧卡顿。当工作列表排空且无新增可达对象时,分析结束,仍带"可能不可达"标志的对象进入后续的清除和销毁流程。

上一篇
下一篇