unity 垃圾回收机制
unity垃圾回收就是底层的内存管理机制
分为两个部分
1.GC部分
由C#创建的引用类型,分配在托管堆上,是由Mono 或者 IL2Cpp来管理
GC工作原理
用三色标记法,最先,所有的节点都是白色,从根节点出发(一般是静态对象,活动栈帧中的局部变量,CPU 寄存器,GC 句柄表。。后面这3个我都不知道是什么),这些根节点标记为灰色,在这些节点中找到他们引用到的对象,将他们标记为灰色,加入灰色列表,同时将拥有引用对象的对象标记为黑色。之后依次遍历灰色列表中存储的对象,找到里面引用到的其他对象,标记灰色。
遍历整个托管堆内存,将仍然还是白色的对象释放,加入空闲内存链表,灰色或者黑色的就重置为白色,等待下次检查。
GC工作步骤:
标记-清除-压缩(移动未被清除的对象,合并空闲内存,减少内存碎片)
GC触发时机:
1.自动触发:托管堆到达阈值,每帧会检查托管堆状况
2.手动触发: System.GC.Collect()
3.场景切换时可能会触发
GC可能会导致主线程卡顿,因为GC是运行在主线程中的,堆越大,对象越多,GC需要的时间越长,清除完成后,还会对碎片进行整理,移动对象到新的内存空间。
运行GC期间,STW(Stop-The-World):遍历期间暂停所有托管线程(保证对象图不变)
GC也会产生很多的堆碎片
常见GC问题来源:
高频实例化:每帧 new 对象(如 Vector3、RaycastHit[])。
字符串操作:频繁拼接字符串(string 不可变,生成垃圾),应该用stringbuilder。
装箱(Boxing):值类型转引用类型(如 int 存入 ArrayList)。
闭包与匿名方法:捕获局部变量时生成额外对象。
诊断GC的工具:
1.Unity Profiler
[file:///C:/Program%20Files/Unity/Hub/Editor/2022.3.57f1c2/Editor/Data/Documentation/en/Manual/ProfilerWindow.html]
2.Memory Profiler 包
3.GC 时间监控
void Update() { System.GC.Collect(); System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); sw.Start(); // ... 代码块 sw.Stop(); Debug.Log($"GC Time: {sw.Elapsed.TotalMilliseconds} ms"); }
GC的优化:
1.减少内存分配(如上面常见来源的错误方式)
2.优化unity某些API调用
3.监控GC触发时机
例如可以手动触发,避免都在一个时间点释放
可以用工具来监测哪里的内存峰值,选择合适的时间点释放
4.使用增量式垃圾回收
在playersetting中开启增量式,让GC回收在多帧完成,减少单帧卡顿
2.非托管内存
Unity 引擎底层(如纹理、音效、Native 插件)使用非托管内存,需手动管理或通过引用计数释放
| 类型 | 示例 | 管理方式 |
|---|---|---|
| Unity 引擎资源 | 纹理(Texture)、网格(Mesh)、音频(AudioClip)、Shader 等 | 引用计数 + 手动释放 |
| Native 插件内存 | C++ 插件、系统 API 分配的内存 | 手动管理(需实现释放逻辑) |
| 第三方库内存 | 如 FMOD、物理引擎(PhysX)分配的内存 | 依赖库自身的释放机制 |
| Graphics API 资源 | GPU 缓冲区(VBO)、渲染纹理(RenderTexture) | Unity 封装管理,但仍需注意泄漏 |
| 资源类型 | 释放方式 |
|---|---|
| AssetBundle | AssetBundle.Unload(true)(卸载资源)或 Unload(false)(保留实例化资源) |
| 纹理/网格 | Resources.UnloadUnusedAssets() + 确保无引用 |
| RenderTexture | RenderTexture.Release() |
| Native 插件 | 调用插件提供的 Release()/Dispose() 方法 |
| 对象实例 | Destroy(gameObject)(移除场景对象) |
GC为什么管不了这些?
因为GC是依靠C#的引用关系来管理对象,对这些资源,第三方库等等的无法监测,非托管的部分基本是unity底层(C++)或者外部类创建的,GC只会回收C#创建的包装器,但是底层资源并没有释放。
例子:
destory(gameObject)
Destroy(gameObject) 操作的对象同时涉及托管内存(C#层)和非托管内存(Unity引擎层),但它们的生命周期管理是分离的。

1.托管内存(C#部分)
| 类型 | 内存位置 | 管理方式 | 示例 |
|---|---|---|---|
| MonoBehaviour | 托管堆 | GC 管理 | Enemy.cs 脚本实例 |
| 脚本中的引用类型字段 | 托管堆 | GC 管理 | public ParticleSystem ps; |
2.非托管部分(Unity 引擎层)
| 类型 | 内存位置 | 管理方式 | 示例 |
|---|---|---|---|
| 场景节点数据 | Native 堆 | 引擎引用计数 | Transform 位置信息 |
| 网格/纹理数据 | GPU 显存 | 引擎资源管理 | Mesh 顶点数据 |
| 物理引擎数据 | PhysX 私有内存 | 物理引擎管理 | Collider 几何体 |
整个释放步骤

1.Destroy(gameObject) 主要处理非托管部分
立即:移除场景节点(非托管)
延迟:释放GPU/物理内存(非托管)
2.C# 对象由 GC 独立管理
即使Native资源释放,托管对象仍在堆中直到GC触发
但重载的 == null 会返回 true(引擎魔术)
3.泄露根源在于:
未通过Unity API释放Native资源,而非GC失效
创建一个gameobject的内存分配:
GameObject go = new GameObject("MyObj")
1.非托管部分:
C++底层,创建一个native object对象,有唯一标识符,状态,标签,层级等,有一个指向transform组件的指针。
如果这个对象有其他的组件,则也会有每个组件在非托管堆创建的native对象
2.托管部分 C#部分
有个托管对象,里面有个指针指向非托管部分的native 对象
挂载在这个对象上面的每个脚本实例

只调用 Destroy(gameObject) 可能导致显存或物理内存泄漏。
因为这个对象可能还挂载着一些组件,例如mesh,collision ,这里会涉及到图集资源,物理数据等
这些资源如果是动态创建的,需要调用api来释放,但是要注意,有些资源是共享的,不能在某个使用它的对象销毁的时候也把这个资源释放,最好通过资源管理类来管理。
浙公网安备 33010602011771号