【原理】Unity GC 对比 C# GC

发布于:2025-08-19 ⋅ 阅读:(14) ⋅ 点赞:(0)

【从UnityURP开始探索游戏渲染】专栏-直达

Unity GC(基于Boehm-Demers-Weiser算法)与标准C# GC(.NET CLR分代算法)的核心对比及优化方向:

⚙️ ‌一、核心机制差异‌

分代回收策略

  • C# GC‌:采用分代模型(Gen0/1/2),优先回收短期对象(Gen0),Gen2回收频率低但耗时长(可达秒级)‌。
  • Unity GC‌:‌不分代‌,每次触发均为Full GC,需遍历整个堆内存,对象越多性能消耗越大‌。

内存碎片处理

  • C# GC‌:标记后执行内存压缩(Mark-Compact),消除碎片‌。
  • Unity GC‌:仅标记清除(Mark-Sweep),‌不压缩内存‌,长期运行易产生碎片,可能触发额外内存申请‌。

线程与暂停机制

  • C# GC‌:支持后台并行回收(Background GC),减少主线程阻塞‌。
  • Unity GC‌:强制 ‌Stop-The-World‌ ,暂停所有线程直至GC完成(即使增量式GC仍需分帧暂停)‌。(停止增量GC时会这样。默认开启 Incremental GC)

⏱️ ‌二、性能影响对比‌

维度 C# GC Unity GC
触发耗时 Gen0:毫秒级;Gen2:秒级 ‌ 随堆内存增长线性上升 ‌
卡顿风险 可控(Gen2低频触发) 高频且不可预测 ‌
内存效率 高(压缩减少碎片) 低(碎片可能引发OOM)‌

三、优化策略差异‌

‌C# GC优化重点‌

  • 减少Gen2回收‌:避免长期存活对象频繁修改(如静态集合)‌。
  • 控制LOH分配‌:大型对象(>85KB)直接进入LOH,需重用或使用ArrayPool‌。
  • 避免装箱拆箱‌:泛型容器替代ArrayList减少堆分配 ‌。

‌Unity GC优化重点‌

  • 降低堆分配频率‌:
    • 避免字符串拼接、闭包、非必要迭代器‌。
    • 值类型替代引用类型(如struct替代class)‌。
  • 对象池技术‌:复用游戏对象(如子弹、特效)‌。
  • 增量GC配置‌:
    • Unity 2019+支持分帧执行GC(Incremental GC),但仅缓解卡顿,不减少总耗时‌。

💎 ‌四、关键结论‌

  • Unity GC更依赖开发者主动优化‌:因缺乏分代和压缩机制,需严格管控堆内存分配‌。
  • 性能敏感场景规避GC‌:
    • 使用栈内存(stackalloc)或预分配内存池‌。
    • 在加载场景或菜单界面‌手动触发GC‌(System.GC.Collect())‌。
  • 监控工具优先级‌:
    • Unity Profiler关注GC Alloc与触发帧率骤降点‌。

⚠️ 注意:Unity 2021+逐步集成‌.NET Core CLR‌,未来可能引入分代GC,但现有项目仍需按当前机制优化‌

https://docs.unity3d.com/cn/2022.3/Manual/performance-incremental-garbage-collection.html

c# 中如果A对象和B对象循环引用了,那么GC会在什么阶段释放他们。

在C#中,即使对象A和对象B存在‌循环引用‌(即A引用B,B引用A),只要它们‌没有任何来自GC根(如静态变量、局部变量、CPU寄存器等)的有效引用‌,垃圾回收器(GC)仍然会在‌下一次回收其所属代龄时正常释放它们‌。具体释放阶段取决于对象所在的代龄和回收触发条件,与循环引用本身无关。

📌 关键机制说明:

标记阶段无视循环引用

GC使用‌标记-压缩算法‌(Mark-Sweep-Compact),从根对象(Roots)出发遍历所有可达对象并标记。若A和B均不被任何根对象直接或间接引用(即不可达),即使二者互相引用,也会被识别为垃圾对象。

循环引用不影响GC对对象可达性的判断,因为标记过程‌仅依赖从根出发的引用链‌,而非对象间的孤立引用环。

释放发生在所属代龄的回收阶段

GC采用‌分代模型‌(0代、1代、2代),对象根据存活时间被分配到不同代:

  • 新对象‌(如局部变量创建的A、B)通常分配在‌0代‌;
  • 若0代堆空间不足,触发‌0代回收‌,此时若A、B不可达,则在此阶段被释放;
  • 若A、B在一次回收后存活,会‌升代‌(如升至1代),后续释放需等待其所属代龄的回收被触发。

释放时机依赖回收触发条件

触发条件 回收范围 循环引用对象释放时机
0代堆满 仅0代 若在0代且不可达,立即释放
1代堆满 0代+1代 若在0/1代且不可达,此轮释放
2代堆满/主动调用GC.Collect 0代+1代+2代(或指定代龄) 若在目标代且不可达,此轮释放

💡 注意事项:

  • 根引用切断是前提‌:必须确保所有指向A或B的外部引用(如局部变量、静态字段)已被置null或超出作用域,否则GC仍会将其视为可达对象。
  • 析构函数(Finalizer)延迟回收‌:若A或B实现了析构函数,即使不可达,首次回收时会被移至终结队列,由独立线程调用析构函数后再真正释放,可能导致延迟。
  • 大对象堆(LOH)的特殊性‌:若循环引用对象大小超过85,000字节,会被分配到大对象堆(属于2代),其回收频率较低,释放可能延迟。

💎 总结:

循环引用对象会在‌其所属代龄的下一次垃圾回收‌中被释放,只要它们从GC根不可达。GC的标记算法天然规避了循环引用导致的内存泄漏问题,开发者无需额外处理引用环。唯一需关注的是及时解除对象与根之间的引用关系。


【从UnityURP开始探索游戏渲染】专栏-直达
(欢迎点赞留言探讨,更多人加入进来能更加完善这个探索的过程,🙏)


网站公告

今日签到

点亮在社区的每一天
去签到