视频加载失败

Unity&C#学习——GC机制

1195 字
6 分钟
Unity&C#学习——GC机制
  1. 1Unity&C#学习——GC机制本文
Unity&C#学习——GC机制

托管内存和原生内存#

托管内存原生/非托管内存
由谁管脚本运行时的GC(Boehm GC)Unity的C++引擎/手动
装什么C#托管对象(托管堆)网格/纹理/音频等资源数据、NativeArray等显式申请的原生内存、引擎内部分配
怎么释放GC自动回收Destroy()/Dispose()/引擎管理
是否产生GC压力是(分配即GC Alloc)否

什么在托管内存里#

  • 引用类型在堆上:class实例、数组、string、委托/闭包、装箱的值类型
  • 值类型在栈上:int/float/struct/

Unity的引擎对象和资源数据#

很多Unity对象(GameObject、Texture2D、Mesh…)的实际资源由C++端持有,C#端只是持有一个“引用”(托管包装对象)

  • GC只能回收这个“引用”,引用背后的资源必须靠诸如Destroy()的手段来显式回收

Unity的GC#

Unity的托管代码(C#)运行在Mono或IL2CPP后端上,都使用Boehm-Demers-Weiser垃圾回收器。

这个垃圾回收器的特性如下:

特性含义和后果
暂停式 Stop-The-WorldGC运行时暂停C#代码运行👉帧时间尖峰
保守式 conservative扫描内存,把看起来像指针的值都当成引用👉无法移动对象,且本该被回收的对象会多活一会儿
非压缩 Non-compacting回收后不整理内存,不调整对象位置👉内存碎片,没有连续空间装大对象,导致堆扩展
非分代 Non-generational每次回收扫描整个堆,不像 .NET 那样分成多代只扫描新对象👉堆越大,每次GC越慢

GC的工作原理#

  • 采用标记-清除法:托管堆上的对象,GC通过“从根出发标记可达对象、清除不可达对象”来回收

分配触发/堆需扩张

暂停托管代码 Stop-The-World

标记Mark 从GCRoots遍历可达对象

清除Sweep 回收未标记对象

恢复执行 堆空间可复用

分配触发/堆需扩张

暂停托管代码 Stop-The-World

标记Mark 从GCRoots遍历可达对象

清除Sweep 回收未标记对象

恢复执行 堆空间可复用

GC Roots:程序可以直接访问到的引用,不需要经过其他对象

根的类型例子
静态字段static List<Enemy> allEnemies
各线程的栈和寄存器正在执行的方法里的局部变量、参数
GC Handle引擎原生层持有的托管对象。比如场景里的每一个MonoBehaviour,原生的C++组件持有对它的C#包装对象的强引用,所以组件只要还在场景中,就不会被回收
其他运行时内部引用正在运行的协程、已注册的委托、被固定的对象
  • 标记:从Roots出发,把所有可达对象标记为“存活”。
  • 清除:未被标记的即垃圾,回收其内存

    但因非压缩,不整理,会留下空洞

  • 触发时机:堆分配时发现需要扩张,或手动GC.Collect()

增量式GC(Incremental GC)#

  • 目的:缓解Stop-The-World的单帧尖峰
  • 流程:将标记阶段拆分到多帧执行,每帧只做一小片

    将“一次大停顿”摊成“多次小停顿”

  • 代价:需要写屏障,且GC总时间可能略增
  • 收益:单帧尖峰显著降低,更容易守住帧预算

工作原理#

  • 分时间片:每帧给GC一个时间预算,用完就暂停标记,记住进度,下一帧接着做

    暂停标记的期间修改引用了怎么办?

  • 三色标记:记录哪些元素已经处理完,哪些还没处理
  • 写屏障:每次给引用类型的字段赋值时,额外执行一小段代码,通知GC“这里的引用变了”,让它重新检查相关对象。

垃圾来源&解决方案#

来源说明解决方案
装箱(Boxing)值类型被当作object使用(如string.Format参数、非泛型集合)不要滥用
字符串操作string不可变,拼接/格式化都分配用StringBuilder、ReadOnlySpan<char>,TMP用text.SetText()零分配
委托、lambda与闭包捕获外部变量的匿名方法会分配闭包对象
返回数组的APIMesh.vertices、GetComponents()、Physics.RaycastAll等每次返回新数组传入预分配数组
Camera.main内部做查找,慢且分配缓存引用
携程yieldyield return new WaitForSeconds(t)每次分配缓存复用
集合扩容List/Dictionary超容量时重新分配底层数组
Instantiate/Destroy频繁创建销毁对象对象池

核心优化手段:复用而非新建、缓存而非重取、预分配而非运行时分配

测量排查方法#

工具用途
Unity Profiler的GC Alloc列看每帧的托管分配量;热路径稳态目标时0B/帧
Deep Profile定位具体哪个调用在分配
Memory Profiler包堆快照,分析对象常驻与泄露
Profiler的GC.Collect标记观察GC何时触发、耗时多少

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Unity&C#学习——GC机制
https://blog-zeta-seven-36.vercel.app/posts/unity_and_csharp-study/gc/
作者
机巧叶片
发布于
2026-10-05
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
机巧叶片
给互联网留下一点微小的贡献
公告
欢迎来到我的博客!这是一则示例公告。
分类
标签
最新动态
站点统计
文章
26
分类
6
标签
22
总字数
27,977
运行时长
0 天
最后活动
0 天前
站点信息
构建平台
Cloudflare Pages
博客版本
Firefly v6.16.8
文章许可
CC BY-NC-SA 4.0
文章目录