Unity3D Boehm GC原理解析

前言

好的,我们来详细解析一下 Unity3D 早期版本中使用的 Boehm-Demers-Weiser 垃圾收集器(通常简称为 Boehm GC 的原理。理解它对理解 Unity 的内存管理历史、某些遗留项目的行为以及"保守式"GC 的特点非常有帮助。
核心概念:Boehm GC 是一个"保守式、非移动、标记-清除"垃圾收集器。

让我们拆解这些术语并解释其在 Unity 上下文中的含义:

对惹,这里有一 个游戏开发交流小组 ,希望大家可以点击进来一起交流一下开发经验呀!

  1. 保守式 (Conservative):
  • 这是 Boehm GC 最核心、最关键的特点,也是它被 Unity 早期选中的重要原因。

  • 原理: GC 在判断一个内存中的值是否是指向堆上对象的指针时,采取"保守"策略。它不依赖于精确的类型信息(如 .NET 运行时提供的元数据)来确定某个内存位置存放的到底是指针还是普通数据(如整数、浮点数)。

  • 工作方式:

    • GC 会扫描所有可能的"根"区域:线程栈(每个线程的调用栈)、CPU 寄存器、全局/静态变量区域等。

    • 对于这些区域中的每一个字(word, 通常是 4 或 8 字节),GC 会检查这个字的值:

      • 它是否落在 GC 管理的堆地址范围内?

      • 它是否指向某个已知分配对象的起始地址(通常是对象头)?

      • 如果满足以上条件,GC 就保守地认为这个字是一个"指针",并将它指向的对象标记为"可达"。
    • 对 Unity 的意义: Unity 引擎核心是用 C++ 编写的,而用户脚本是用 C# 编写的。两者通过复杂的互操作(P/Invoke, Marshaling)交互。C++ 代码中会有指向托管 C# 对象的不透明指针(IntPtr),C# 代码也会通过 P/Invoke 调用 C++ 函数并传递数据。Boehm GC 的保守性使它不需要精确知道 C++ 这边哪些变量是托管对象的指针,它只要看到一个值像指针(在堆地址范围内且对齐),就认为它是。这极大地简化了 Unity 引擎与 Mono/.NET 脚本运行时之间的集成,避免了对 C++ 引擎进行大规模改造以提供精确指针信息。
  1. 非移动 (Non-Moving / Non-Compacting):
  • 原理: 在垃圾回收过程中(特别是清除阶段之后),Boehm GC 不会移动存活的对象来压缩堆空间。回收的内存块("洞")就留在原地。

  • 优点: 实现相对简单。避免了移动对象带来的复杂性和开销(需要更新所有指向该对象的指针)。

  • 缺点: 主要问题是内存碎片化。频繁的分配和回收会导致堆中出现大量大小不一、分散的空闲内存块。虽然 GC 有算法(如空闲列表)来尝试分配合适大小的对象到这些空闲块中,但最终可能导致:

    • 即使总空闲内存足够,也可能因为找不到足够大的连续空闲块而导致分配失败(触发 GC 或抛出 OutOfMemoryException)。

    • 降低缓存局部性,可能影响性能。

    • 对 Unity 的意义: 这是 Unity 早期版本中内存问题(尤其是长时间运行的游戏出现内存缓慢增长或卡顿)的一个常见原因。开发者需要更积极地管理大对象池或注意分配模式来缓解碎片化。
  1. 标记-清除 (Mark-Sweep):
  • 这是 Boehm GC 使用的基本回收算法,分为两个主要阶段:

    • 标记阶段 (Mark):

      • 从所有"根"(栈、寄存器、全局变量)开始遍历。

      • 使用上面描述的保守式方法找出所有根直接引用的对象,将它们标记为"可达"(通常是在对象头中设置一个标志位)。

      • 递归地遍历这些可达对象所引用的所有其他对象(同样保守地识别指针),也将它们标记为"可达"。这个过程持续到所有从根出发可达的对象都被标记。

      • 清除阶段 (Sweep):

        • GC 线性遍历整个堆。

        • 对于堆中的每一个对象:

          • 如果被标记为"可达",则清除标记位(为下次 GC 准备),该对象存活。

          • 如果没有被标记,则认为它是垃圾,将其占用的内存块回收,并加入到空闲内存列表中以供后续分配使用。

        • 注意: 清除阶段只回收内存,不移动存活对象(非移动性)。

Boehm GC 在 Unity 中的工作流程概要:

  1. 分配: 当脚本代码(C#)使用 new 创建对象时,内存分配请求会发给 Boehm GC 管理的内存池。GC 尝试在堆的空闲块中找到合适大小的空间。如果找不到,则触发一次垃圾回收。
  2. 触发回收: 回收通常在:
  • 分配请求无法满足时(如上所述)。

  • 开发者显式调用 System.GC.Collect() (通常不建议)。

  • GC 根据自身启发式策略(如分配总量、时间阈值等)决定触发。

  1. 暂停程序 (Stop-The-World): 传统的 Boehm GC 在执行完整的标记-清除回收时,需要暂停所有托管线程。这是为了避免在回收过程中程序修改对象图(创建新引用、移除旧引用、分配新对象),导致标记错误。这是 GC 引起卡顿的主要原因之一。
  • 注意:Unity 后期对 Boehm GC 进行了定制和改进,引入了增量式垃圾回收(Incremental GC),试图将标记工作分散到多帧进行,显著减少了单次停顿时间,但核心原理不变。
  1. 保守式标记: 暂停后,GC 扫描所有根区域(所有线程栈、寄存器、全局变量),保守地识别可能的指针,并递归标记所有可达对象。
  2. 清除: 遍历堆,回收所有未被标记的对象占用的内存(加入空闲列表),清除存活对象的标记位。
  3. 恢复程序: 恢复所有托管线程执行。

Boehm GC 的优缺点总结 (在 Unity 上下文中):

  • 优点:

    • 实现相对简单: 核心算法比精确式、分代式、移动式 GC 简单。

    • 语言/环境中立: 不需要语言运行时提供精确的类型和指针信息。这是它能无缝集成 Unity C++ 引擎和 C# 脚本的关键。

    • C/C++ 友好: 对通过 P/Invoke 暴露给 C# 的 C/C++ 代码中的指针处理比较宽容。

    • 对"野指针"有一定容忍度: 如果野指针碰巧指向一个存活对象,该对象不会被错误回收(但可能掩盖真正的 bug)。

  • 缺点:

    • 内存碎片化: 非移动性导致的主要问题,影响长期运行的应用程序性能和稳定性。

    • 保守性导致内存泄漏: 这是最大的理论缺点。如果一个整数值、浮点数或字符串数据恰好等于一个已死亡对象的地址,GC 会误认为它是有效指针,从而错误地将那个死亡对象标记为"可达",阻止其回收。虽然实践中发生的概率不是极高,且可以通过避免在指针位置存储非指针数据来缓解,但理论上存在,且无法完全避免。这被称为"保守式收集器的内存泄漏"。

    • 潜在的性能开销: 扫描整个栈和全局区域可能比精确式 GC 扫描已知的指针位置开销大。标记过程需要遍历所有存活对象。

    • Stop-The-World 停顿: 完整 GC 会导致程序暂停,影响响应性(增量 GC 缓解了此问题)。

Unity 的演进:

Unity 早期(大致 Unity 5.x 及更早)主要使用 Mono 运行时配套的 Boehm GC。随着对性能、内存占用和卡顿优化的要求不断提高:

  1. 增量式 Boehm GC: Unity 对 Boehm GC 进行了深度定制,实现了增量式垃圾回收。它将耗时的标记阶段分成很多小步骤,穿插在游戏逻辑帧之间执行,大大减少了单次停顿时间(从几十毫秒降到几毫秒甚至更低),显著改善了游戏流畅度。但碎片化和保守性泄漏问题依然存在。
  2. 替换为 Unity 新的 GC: 为了解决 Boehm GC 的根本性缺点(主要是碎片化和保守性),Unity 投入巨资开发了全新的垃圾收集器(通常称为 Unity GCBoehm 的替代者)。这个新 GC:
  • 精确式的:需要并利用了 .NET/Mono 运行时提供的精确类型信息,能准确识别指针,避免了保守性泄漏。
  • 分代式的:将对象按生命周期长短分为不同代(通常是 0代,1代,2代),优先收集最可能死亡的年轻代对象(0代),大幅提高收集效率,减少每次 GC 需要扫描的对象数量。
  • 压缩式 的(至少对部分代):在回收后会移动存活对象,压缩堆空间,彻底解决碎片化问题。这需要精确的指针信息来更新所有引用。
  • 通常是并发/增量式的:进一步减少 STW 停顿。
  • 这个新 GC 从 Unity 2017.x 左右开始逐步替换 Boehm GC,并在后续版本中成为默认和推荐的 GC。

总结:

Unity3D 早期使用的 Boehm GC 是一个保守式、非移动、标记-清除 垃圾收集器。它的核心价值在于其保守性 ,这使得它能够相对简单地集成 Unity 的 C++ 引擎核心和 C# 脚本运行时,无需精确追踪 C++ 中的托管对象指针。然而,这也带来了保守性内存泄漏 的风险,并且其非移动性 导致了严重的内存碎片化 问题。后期通过引入增量式回收 显著改善了卡顿问题。最终,Unity 开发了全新的精确式、分代式、压缩式垃圾收集器来克服 Boehm GC 的根本缺陷,提供了更好的性能、更低的内存占用和更少的内存碎片。理解 Boehm GC 的原理对于理解 Unity 内存管理的历史、遗留项目的优化以及不同 GC 策略的权衡至关重要。

更多教学视频

Unity3D​www.bycwedu.com/promotion_channels/2146264125

相关推荐
知识分享小能手40 分钟前
Vue3 学习教程,从入门到精通,使用 VSCode 开发 Vue3 的详细指南(3)
前端·javascript·vue.js·学习·前端框架·vue·vue3
前端小趴菜053 小时前
react状态管理库 - zustand
前端·react.js·前端框架
Thomas游戏开发10 小时前
Unity3D 文件夹注释工具
前端框架·unity3d·游戏开发
liangshanbo121511 小时前
微前端框架对比
前端框架
NetX行者11 小时前
基于Vue 3的AI前端框架汇总及工具对比表
前端·vue.js·人工智能·前端框架·开源
伍哥的传说12 小时前
H3初识——入门介绍之常用中间件
前端·javascript·react.js·中间件·前端框架·node.js·ecmascript
遂心_13 小时前
深入剖析React待办事项应用:Hooks、组件化与性能优化实战
前端·react.js·前端框架
摸鱼仙人~1 天前
styled-components:现代React样式解决方案
前端·react.js·前端框架
Baklib梅梅1 天前
Ruby大会演讲实录:Baklib 如何用 AI 重构内容管理赛道
ruby on rails·前端框架·ruby