前言
- 本文尝试通过剖析 UE4 的源码和运行时调用堆栈,借助时序图和流程图,来说清楚 UE4 垃圾回收机制的调用流程、内部流程、标记(可达性分析)过程等。也简单提到了 UE4 中的垃圾回收规范、通用垃圾回收机制分类等,希望能帮助大家更好地理解和适应、使用 UE4 的垃圾回收机制。
- 本文参考了网上较多相关文档,除各种流程尤其是各种UML图之外,部分文字甚至直接借鉴而来。感谢这些技术分享者。
垃圾回收是什么
- 在计算机科学中,垃圾回收(英语:Garbage Collection,缩写为GC)是指一种自动的内存管理机制。当某个程序占用的一部分内存空间不再被这个程序访问时,这个程序会借助垃圾回收算法向操作系统归还这部分内存空间。垃圾回收最早起源于LISP语言。目前许多语言如Smalltalk、Java、C#、Go和D语言都支持垃圾回收器。
- 垃圾回收器有两个基本的原理:
- 考虑某个对象在未来的程序执行中,将不会被访问。
- 回收这些对象所占用的内存。
为什么需要垃圾回收?
- 垃圾回收器可以减轻程序员的负担,也减少程序中的错误。
- C 和 C++ 等低级语言不提供开箱即用的垃圾收集器。这意味着您必须手动跟踪正在使用的内存并在您不再希望使用它时释放它。这很容易出现错误并且程序员更难管理,因此虚幻引擎 4 创建了自己的垃圾收集系统。
- 如果函数内部有指针,则不必担心垃圾回收系统。函数内的这些指针的作用类似于普通的 C/C++指针,不需要任何更改。
- 但是,如果要使用指向某个对象的指针,并使其存在多个帧,则需要注意垃圾回收。
UE4 中如何支持垃圾回收?(垃圾回收的规范和约束)
概要
- 指针必须存储在类中的成员变量中,并且必须在它之前添加 UPROPERTY(...) 宏,以便通知 Unreal Build Tool 自动生成您需要的代码,使对象与 Unreal 的垃圾收集系统正常工作。
- 当您销毁一个 Actor 时,它将在帧结束时将自己从世界中移除。这意味着它将继续存在(并且指针仍然有效)直到帧结束,那时它们才会因为已经被删除了而变为 null 。可以使用 IsValid 来知道指向对象的指针是否仍然有效以及该对象没有被销毁的情况。在该 IsValid(...)函数中传递一个指向对象的指针。如果指针为 nullptr,或者已在该对象上调用 Destroy() 并且尚未从世界中移除,则 IsValid() 将返回 false。
需要为 float/int32/etc 做什么操作来支持GC吗?
不需要! 从 UObject 派生的类是唯一可以考虑进行垃圾收集的东西。结构和标准数据类型不需要内存管理,因此不是垃圾收集系统的一部分。 您可能仍会在这些数据类型上看到 UPROPERTY() 宏,但这通常是为了将它们暴露给蓝图,而不仅仅是用于垃圾收集系统。
如何管理非 UE C++ 类的内存?
- UPROPERTY() 宏只能用于从“UObject”驱动的类。如果您需要管理非虚幻 C++ 类的内存,需要手动管理内存。
- 使用 TWeakObjectPtr/FWeakObjectPtr,它不会使对象保持活动状态,但在对象被销毁后调用这两种数据类型的 IsValid 方法时,它将返回 false。
- 这对于保持对另一个对象的引用并查看该引用是否仍然有效(但不实际需要负责保持该对象的活动)非常有用。这通常被认为是一个高级用例,大部分时间都不需要。
可以手动管理内存吗?
您可以通过 new 关键字创建新实例,也可以使用 delete 将其删除。调用删除并释放内存非常重要,否则将产生内存泄漏。这些普通的C++类应包含在虚幻引擎公开的类(如Actor或UObject)中,当该Actor/UObject被破坏时,您应该释放内存。请记住,它是手动管理的,这意味着如果您分配内存,则负责删除内存。
具体编码实践
参见:(UE4 4.20 )UE4的GC(垃圾回收)编程规范
UE4 的垃圾回收流程
垃圾回收全流程概要
概述
- 通过 UE4 反射机制把 Object 添加到容器中管理;
- 在地图加载、卸载或Package保存、Tick中定时调用 GC ,或按需手动调用 GC ;
- UE 的垃圾回收也分为标记和删除阶段,在此基础上多了一个簇和增量回收的概念。使用簇的目的是为提高回收效率,增量回收为优化垃圾回收时的卡顿。
流程图
Object 的收集注册流程
概述
垃圾回收时需要根据 Object 的索引从 GUObjectArray 中获取对应的 Object ,这个容器的数据依赖 UE4 的反射机制,通过相关宏,由 UHT 生成的代码,在引擎启动等处自动注册。底层会通过 StaticAllocateObject 最终调用 AllocateUObjectIndex 添加(绑定)到容器中。
时序图
垃圾回收的触发流程
概述
- UE4 中的垃圾回收系统会自动触发,也可手动触发。
- 手动触发
- 可以在执行一些操作时手动调用GC,比如卸载一个资源后,立即调用一次GC进行清理。
- 而且方式有多种,游戏中可以调用 ForceGarbageCollection 来让World下次tick时进行垃圾回收。也可以直接调用CollectGarbage进行垃圾回收,引擎中大部分情况都用这种方式主动引发。
- 自动触发
- 游戏中,大部分的垃圾回收操作都是由UE4自动引发的,普通情况下不需要手动调用GC,这也是理想的GC使用方式。
- 当World进行tick时,会调用UEngine::ConditionalCollectGarbage()函数,函数中进行了一些判断,当满足GC条件时,才会执行GC。
时序图
垃圾回收的内部流程
概述
UE4 的垃圾回收流程简单来说也分为标记和删除阶段,只不过它多了一个簇(cluster)和增量回收的概念。使用簇的目的是为提高回收效率,增量回收为优化垃圾回收时的卡顿。
流程图
Object 的可达性分析(标记)流程
概述
UE4 先把所有被管理起来的 Object 标记为不可达,然后借助 ReferenceTokenStream 的概念,来生成和获取引用信息,为还在使用的 Object 设置可达标记,以免被 GC 。
流程图
增量回收流程
概述
经过了标记过程,那些还有不可达标记的物体可以进行删除了。为减少卡顿,UE 加入了增量清除的概念(IncrementalPurgeGarbage()),就是一次删除只占用固定的时间片。当然,如果是编译器状态或者是强制完全清除(比如下一次GC了,但是上一次增量清除还没有完成),那么就会强制清除。
流程图
流程较为简单,读者可直接查看 UE4 源码中的 IncrementalPurgeGarbage 函数来了解。
扩展阅读:通用垃圾回收分类概述
根据收集器的实现方式分类
引用计数收集器
最早的也是最简单的垃圾回收实现方法,这种方法为占用物理空间的对象附加一个计数器,当有其他对象引用这个对象时计数器加一,反之引用解除时减一。这种算法会定期检查尚未被回收的对象的计数器,为零的话则回收其所占物理空间,因为此时的对象已经无法访问。这种方法无法回收循环引用的存储对象。
跟踪收集器
近现代的垃圾回收实现方法,这种算法会定期遍历它管理的内存空间,从若干根储存对象开始查找与之相关的存储对象,然后标记其余的没有关联的存储对象,最后回收这些没有关联的存储对象占用的内存空间。
根据回收算法分类
标记-清除
先暂停整个程序的全部运行线程,让回收线程以单线程进行扫描标记,并进行直接清除回收,然后回收完成后,恢复运行线程。这样会产生大量的空闲空间碎片,和使大容量对象不容易获得连续的内存空间,而造成空间浪费。
标记-压缩
和“标记-清除”相似,不同的是,回收期间同时会将保留的存储对象搬运汇集到连续的内存空间,从而整合空闲空间,避免内存碎片化。
复制
需要程序将所拥有的内存空间分成两个部分。程序运行所需的存储对象先存储在其中一个分区(定义为“分区0”)。同样暂停整个程序的全部运行线程,进行标记后,回收期间将保留的存储对象搬运汇集到另一个分区(定义为“分区1”),完成回收,程序在本次回收后将接下来产生的存储对象会存储到“分区1”。在下一次回收时,两个分区的角色对调。
增量回收器
需要程序将所拥有的内存空间分成若干分区。程序运行所需的存储对象会分布在这些分区中,每次只对其中一个分区进行回收操作,从而避免暂停所有正在运行的线程来进行回收,允许部分线程在不影响回收行为下保持运行,并且降低回收时间,增加程序响应速度。
分代
由于“复制”算法对于存活时间长,大容量的储存对象需要耗费更多的移动时间,和存在储存对象的存活时间的差异。需要程序将所拥有的内存空间分成若干分区,并标记为年轻代空间和年老代空间。程序运行所需的存储对象会先存放在年轻代分区,年轻代分区会较为频密进行较为激进垃圾回收行为,每次回收完成幸存的存储对象内的寿命计数器加一。当年轻代分区存储对象的寿命计数器达到一定阈值或存储对象的占用空间超过一定阈值时,则被移动到年老代空间,年老代空间会较少运行垃圾回收行为。一般情况下,还有永久代的空间,用于涉及程序整个运行生命周期的对象存储,例如运行代码、数据常量等,该空间通常不进行垃圾回收的操作。 通过分代,存活在局限域,小容量,寿命短的存储对象会被快速回收;存活在全局域,大容量,寿命长的存储对象就较少被回收行为处理干扰。 现今的GC(如Java和.NET)使用分代收集(generation collection),依照对象存活时间的长短使用不同的垃圾收集算法,以达到最好的收集性能。
参考链接
- 总结:讲了下UE的GC流程(主要是 CollectGarbageInternal 里的代码)和相关的卡顿优化, 主要讲怎么优化GC
- 总结:文字较多,比较详细地介绍了UE的GC流程的每一步。
- 总结:较为深入地分析了各GC分类
- 总结: Object的各种特性的简介,如GC,反射,序列化,网络同步等
- 总结:从使用角度浅显地说明了UE的GC机制。
- 总结: Gameplay 的简单框架,各种总结图,也提到了GC、反射、序列化等
- 总结:列举了UE的C++中各种需要GC的场合下应该如何使用
- 总结* 比较深入地分析了标记和清除中如何标记等的代码
- 总结:简单提到了垃圾回收的分类,但是借助源码和类图、流程图详细分析了UE的GC流程,包括但不限于标记和清除过程。
- 总结:言简意赅,罗列了关键的函数
**声明:**本文来自公众号:GameDevLearning,转载请附上原文链接 从源码剖析 UE4 的垃圾回收机制 及本声明。