GC

常见GC垃圾回收算法

三种最基本的GC算法是标记-清除法、引用计数法、GC复制算法。其余很多是这几种的组合。

引用计数法:

计数器为0则回收。所以可即刻回收垃圾。

标记-清除法

全量标记,增量清除。遍历对象并标记,再对不可达对象收集到一个链表里进行回收。

GC复制算法

对象分配在from空间,from空间占满时,把这些对象复制到to空间,然后互换from空间和to空间。 所以不会有碎片化问题,也不需要挨个回收,但是对象会移动位置,且每次只能利用一半的堆空间。 GC复制算法的示意图

分代垃圾回收

把对象分为几代,将存活了一定次数的新生代对象当作老年代对象来处理,所以可以较少被gc; 新生代空间则可以用 GC 复制算法。 分代垃圾回收的堆空间

增量式垃圾回收

标记清楚时的全量标记耗时又不能分帧,会造成卡顿。所以诞生了增量式GC。一般用三色标记法。

三色标记法是将对象根据搜索情况,分为三种颜色:

  • 白色:还未搜索过的对象
  • 灰色:正在搜索的对象
  • 黑色:搜索完成的对象

标记阶段就先标记灰色,再对灰色的子节点标记灰色,当前节点则标记黑色,依次下去。

但这样在并发时可能有问题,需要导致对象丢失,所以需要有屏障机制,一般会有写入屏障。

参考

https://zhuanlan.zhihu.com/p/690601125

UE的GC

基础算法

标记-清楚算法,分成三个部分:所有UObject先标记不可达,可达性分析,销毁垃圾。

垃圾收集:

  1. GC.MarkObjectsAsUnreachable: 标记所有对象不可达,保存所有Root对象。
  2. GC.PerformReachabilityAnalysisOnObjectsInternal: 遍历所有Root对象的引用对象,标识为可达。
  3. GatherUnreachableObjects: 遍历所有UObject,收集所有不可达对象到 GUnreachableObjects

不可达对象清除:

  1. UnhashUnreachableObjects:

    遍历所有不可达对象,调用ConditionalBeginDestroy,完成销毁前的清理工作,可自定义实现BeginDestroy。调用UnhashObject,从FUObjectHashTables移除。

    此处会提交贴图内存资源销毁操作到RHICommandList,由渲染线程执行销毁。

  2. IncrementalDestroyGarbage

UObject、继承FGCObject的对象可被 GC。通过UClass的反射信息可以在可达性分析时快速拿到对象的引用关系。

UObject、FGCObject的收集

  • GUObjectArray:UE 中存储所有 UObject 实例的全局数组(准确说是 TArray + 分块存储),是 UObject GC 的「根目录」,所有 UObject 的创建 / 销毁都要注册 / 注销到这里。
  • FGCObject:一个接口类,用于让非 UObject 类型(如 STL 容器、自定义 C++ 类)参与 UObject 的 GC 引用追踪,避免 UObject 被误回收。

GUObjectArray

GUObjectArray 中不直接存储 UObject 指针,而是存储 FUObjectItem 结构体 —— 它是 UObject 的「元数据包装器」,负责记录 UObject 的生命周期、GC 状态、索引等核心信息。 FUObjectItem 有 Flag 判断是不是root,简化示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
struct FUObjectItem
{
    // 核心:指向对应的UObject实例(真正的对象指针)
    UObject* Object = nullptr;

    // GC相关标记位(最关键)
    EObjectFlags ObjectFlags; // 对象标记,包含Root标记、GC相关标记等
    EInternalObjectFlags InternalFlags; // 内部标记,如是否被GC标记为可达

    // 索引与管理信息
    int32 ClusterIndex; // 分块存储的集群索引(优化内存)
    int32 SerialNumber; // 序列化编号(唯一标识UObject)

    // 生命周期状态
    EObjectLifeCycle LifeCycle; // 对象生命周期阶段(如未初始化、已初始化、待销毁)

    // 调试与辅助信息
    FName ClassName; // UObject所属类名
    uint32 HashNext; // 哈希表下一个节点(用于快速查找)
};

FGCObject

FGCObject 本身不是 UObject,当你创建一个继承自 FGCObject 的类实例时,它的构造函数会自动将自己注册到一个全局的单例对象——GGCObjectReferencer 上。这个 GGCObjectReferencer 本身是一个特殊的、被引擎牢牢持有的 UObject。

正因为 GGCObjectReferencer 是一个被持有的 UObject,它本身就在GC的根集之中。当GC进行可达性分析时,会遍历到这个引用器。

在遍历 GGCObjectReferencer 时,GC会调用它管理的所有 FGCObject 的 AddReferencedObjects() 函数。而你需要在 FGCObject 子类中实现的 AddReferencedObjects() 函数里,做的就是将你所持有的 UObject 指针通过 FReferenceCollector 对象“上报”给GC。

Cluster优化

如果一堆UObject生命周期与一个节点一致,则可以绑成一个 Cluster,这个节点则为 Cluster Root。可达性分析时每次遍历到这个 Cluster 时就可以认为整个 Cluster 里所有对象都可达。

AddToCluster 可以把一个UObject加入到一个 Cluster 里。

1
2
3
4
5
6
/**
* Adds this objects to a GC cluster that already exists
* @param ClusterRootOrObjectFromCluster Object that belongs to the cluster we want to add this object to.
* @param Add this object to the target cluster as a mutable object without adding this object's references.
*/
COREUOBJECT_API void AddToCluster(UObjectBaseUtility* ClusterRootOrObjectFromCluster, bool bAddAsMutableObject = false);

UE5的GC改动

加入引用计数机制

主要是 StrongObjectPtr 使用,通过 RAII 在构造析构时操作 RefCount。可以减少可达性分析时间,同时单独针对StrongObjectPtr进行了优化。

增量式可达性分析

增量式的垃圾回收,目前还是Experimental阶段。

对于UE的GC的三个部分:所有UObject先标记不可达,可达性分析,销毁垃圾。最后销毁垃圾是可以分帧的,但是前两个部分是不能分帧的。而可达性分析阶段是耗时主要部分。

这里主要要考虑写入屏障,UE 引入 TObjectPtr,使得A.XX = B这种语句能被捕获到了。

可达性分析的批处理

UE5将传统的、庞大的单次引用遍历,优化为基于“批”(Batch)的处理方式。它将处理逻辑拆解为多个对CPU缓存更友好的小函数,并以大约500个对象为一个批次进行处理,显著提升了标记阶段的性能。

参考

https://zhuanlan.zhihu.com/p/1991647341095179509

Unity的GC

基础

Unity 有两个脚本后端:Mono 和 IL2CPP (Intermediate Language To C++),它们各自使用不同的编译技术:

  • Mono 使用即时 (JIT) 编译,在运行时按需编译代码。
  • IL2CPP 使用提前 (AOT) 编译,在运行之前编译整个应用程序。

Unity的托管堆是游戏脚本(如C#代码)中所有引用类型对象(比如类实例、数组、字符串)存放的内存区域。它的生命周期由垃圾回收器自动管理,这也是“托管”的含义。

所以Unity的GC一般是指托管堆的这部分,采样 Boehm 算法。实际上是标记清楚算法,默认是增量式的,也就是有三色标记法、写入屏障等。

参考

https://docs.unity3d.com/cn/current/Manual/performance-managed-memory.html

https://zhuanlan.zhihu.com/p/1922386751520997859

Licensed under CC BY-NC-SA 4.0