前言✨
1、GC 垃圾回收是什么?
2、GC的工作流程?
3、什么会产生 GC(重点,Unity 考点)
4、Unity 中减少 GC 的手段
5、GC 触发时机是什么?
6、【补充知识:内存泄漏】
常考问题:
Q:物体 Destroy 了,Mono 脚本一定被 GC 回收吗?(了解)
Q:协程为什么会造成内存泄漏?(了解)
首先先明确GC必须要学习的几个知识点:
1、GC 垃圾回收是什么?
2、GC的工作流程?
3、什么情况会产生 GC?
4、Unity 中减少 GC 的手段?
5、GC 触发时机是什么?
6、【补充知识:内存泄漏】
接下来我将一个一个来讲讲我的理解:
1、GC 垃圾回收是什么?
GC 全称 Garbage Collector,垃圾回收器。C# 是托管语言,不需要开发者手动释放堆内存,GC 自动扫描堆,回收没有任何引用指向的对象,释放堆内存。栈内存不受 GC 管理,函数执行完自动释放。
2、GC的工作流程?
工作流程:
- 标记阶段:从根对象出发扫描,所有能访问到、有引用的堆对象标记为存活。根对象包括静态变量、Mono 脚本、局部变量引用。
- 清除阶段:把没有标记、没有引用的对象判定为垃圾,回收内存。
- 压缩(整理)阶段:压缩的时候会 STW,暂停主线程,这就是 Unity 卡顿的来源。
STW:Stop‑The‑World,所有用户逻辑暂停,GC 工作的时候游戏会卡帧。
3、什么会产生 GC(重点,Unity 考点)
- new 引用类型对象 class、数组,在堆分配内存。
- 装箱拆箱:值类型转 object,堆上生成对象。
- 字符串频繁拼接,string 每次 + 生成新字符串。
- 闭包 lambda捕获局部变量,编译器生成隐藏类,堆分配。
- foreach 旧版本迭代器、协程,会生成迭代器对象。
- 集合扩容:List/Dictionary 容量不足,内部 new 新数组。
4、Unity 中减少 GC 的手段
- 尽量减少 Update 里面 new 引用对象,不要每帧 new。
- 优先泛型集合List<T>,避免非泛型集合带来装箱。
- 大量字符串拼接用StringBuilder。
- 对象池,复用物体、组件,减少频繁 Instantiate/Destroy,减少 new。
- 集合预先设置 Capacity,避免运行时扩容。
- 少在循环、Update 写 lambda 闭包捕获局部变量。
- 及时置空引用,解除静态、单例对废弃对象的持有。
- 可以手动调用GC.Collect()强制回收,但不建议频繁调用,会带来 STW 卡顿,一般只适合场景切换的时候调用一次。
5、GC 触发时机是什么?
GC 触发时机有三种:
- 第一,new 新对象的时候,托管堆剩余内存不够分配,就自动触发回收
- 第二,代码手动调用 GC.Collect 强制回收,游戏业务尽量不要手动调用
- 第三,IL2CPP/Mono 内部达到内存阈值自动触发,不同平台阈值会不一样
6、【补充知识:内存泄漏】
内存泄漏就是程序中已经不再使用的对象,还被其他地方持有引用,GC 无法对它进行回收,这部分内存得不到释放。随着运行时间变长,内存持续上涨,会出现卡顿,严重会造成内存溢出、闪退。
Unity 里面最常见容易出现内存泄漏的几个原因:
第一,事件订阅忘记移除监听。【比如说弹窗 UI 在 OnEnable 订阅金币变化事件,关闭弹窗直接 Destroy 物体,没有在 OnDisable 移除监听。物体销毁,但事件中心字典仍然保存委托,不仅会后续广播时报空引用,Mono 对象也无法 GC 回收。】
第二,静态变量或者单例持有已经销毁的 Mono 对象。【比如单例管理器用静态变量保存玩家角色,场景切换角色被销毁,但静态变量没有置空,产生伪 null,对象得不到释放;Mono 单例 OnDestroy 忘记清空 Instance 也属于这种情况。】
第三,协程没有停止。【如果协程是别的物体启动的,目标物体被 Destroy 之后协程不会自动停止,协程迭代器会持有销毁物体的引用,造成泄漏;注意物体自身启动的协程,物体销毁会自动停止。】
第四,List、Dictionary 这类集合持续添加数据,没有做清理。【例如战斗伤害飘字不断加入 List,战斗结束只 Destroy UI 物体,没有 Clear 集合,集合一直持有对象引用,内存持续累积上涨。 】
开发规范:OnEnable 订阅事件,OnDisable 移除;物体销毁时停止协程;不要用静态变量缓存临时游戏物体;Profiler 的 Memory 面板可以用来排查内存泄漏。
常考问题:
Q:物体 Destroy 了,Mono 脚本一定被 GC 回收吗?(了解)
话术:
不一定。如果还有静态变量、事件委托、未停止的协程持有这个对象引用,C# 对象不会被 GC 回收,会出现伪 null。引擎层物体销毁,C# 对象还留在内存,就是内存泄漏。
Q:协程为什么会造成内存泄漏?(了解)
话术:
物体被 Destroy,如果没有 StopCoroutine,协程还会继续执行。协程的 IEnumerator 会持有 Mono 对象,造成对象无法 GC 回收,引发内存泄漏。物体销毁会自动停止该物体上所有协程,但如果协程是其他对象启动的就不会自动停止。
拓展知识:托管内存和非托管内存(先简单讲讲后续我会出详细的介绍)
托管内存由 .NET GC 管理;非托管内存或非托管资源不由 GC 直接管理,需要开发者显式释放。
重点注意:GC 负责回收托管对象的内存,但不等于所有资源都自动释放。涉及文件、网络、数据库、Unity Native 资源时,要实现或调用 IDisposable.Dispose() ,优先用 using ,必要时用 SafeHandle 管理非托管句柄。