♻️ Unity GC 内存优化完全指南:彻底告别卡顿的终极手册
💡 GC 优化的价值 :
游戏每隔几秒就卡一下,GC 导致的?
想减少 GC 触发次数,却不知道怎么做?
对象池、缓存、值类型,哪个更适合你的场景?
想深入理解 GC 原理,从根本上解决问题?
这篇文章! 将深入解析 Unity GC 机制,从原理到实践,提供 24 个实战优化技巧,让内存分配更高效!
一、内存管理方式概览 1.1 三种内存管理模式
模式
代表技术
优点
缺点
手动管理
C/C++ malloc/free
速度快,无额外开销
容易内存泄露、野指针
半自动管理
引用计数 (Reference Count)
自动回收,速度快
存在循环引用问题
全自动管理
追踪式 GC (Tracing GC)
无需关心释放
有性能开销
1 2 3 4 5 6 7 8 9 10 11 12 13 ┌─────────────────────────────────────────────────────────────────────────┐ │ Unity GC 工作原理 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ Mark 阶段: Sweep 阶段: │ │ ┌─────────┐ ┌─────────┐ │ │ │ GC Root │───标记可达对象──────> │ 回收未标记 │ │ │ └─────────┘ │ 对象 │ │ │ │ └─────────┘ │ │ ▼ │ │ 所有可达对象被标记 │ │ │ └─────────────────────────────────────────────────────────────────────────┘
二、Unity GC 特性 2.1 Boehm-Demers-Weiser GC Unity 使用的 GC 器具有以下特点:
特性
说明
影响
Stop The World
GC 时所有线程必须停止
会造成卡顿
不分代
整个托管堆统一扫描
GC 速度较慢
不压缩
不进行碎片整理
产生内存碎片
⚠️ 重要 :即使 Unity 2019+ 的增量式 GC,回收时仍需停止所有线程。
2.2 内存碎片问题 1 2 3 4 5 6 堆内存布局 (不压缩 GC): ┌───┬────┬─┬──────┬─┬──┬────────┬─┬────┐ │ A │ ■ │B│ ■ │C│ ■│ D │ ■│ E │ └───┴────┴─┴──────┴─┴──┴────────┴─┴────┘ ■ = 空白间隙 (无法合并)
当申请新对象时,如果没有单个间隙大于对象大小,堆内存就会增加。
三、影响 GC 性能的核心因素 3.1 可达对象数量 减少对象数量的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 class Item { public int a; public short b; } Item[] items; class Item { public int [] a; public short [] b; } Item item;
3.2 托管堆大小 优化策略:
减少临时分配 - 使用初始化分配和预分配
减少内存泄露 - 防止孤岛效应和循环引用
四、类与结构体 4.1 内存结构对比 1 2 3 4 5 6 7 8 9 10 11 12 类对象内存布局 (堆中): ┌──────────┬──────────┬────────────────┐ │ vtable │ Monitor │ 字段数据 │ │ (IntPtr) │ (IntPtr) │ (对齐排列) │ └──────────┴──────────┴────────────────┘ 4/8字节 4/8字节 根据字段 结构体内存布局 (栈中): ┌──────────────────────┐ │ 字段数据 │ │ (紧凑对齐) │ └──────────────────────┘
类型
存储位置
对齐建议
类
堆 (实例)
尽量紧凑排列
结构体
栈 / 堆 (装箱)
遵守对齐规则
4.2 装箱拆箱场景
场景
是否装箱
解决方案
结构体 → 接口
✅ 是
避免接口转换
值类型.GetType()
✅ 是
缓存类型
结构体.ToString()
Mono/IL2CPP 不同
重写方法
容器类操作
部分是
使用泛型
五、参数传递优化 5.1 值传递 vs 引用传递 1 2 3 4 5 void Calc1 (UnityEngine.Ray ray2 ) { }void Calc1 (ref UnityEngine.Ray ray2 ) { }
5.2 ref、in、out 对比
关键字
含义
使用场景
ref
可读写引用
大于 IntPtr.Size 的结构体
in
只读引用
不应被修改的参数
out
只写引用 (类似返回值)
多返回值
5.3 ref return 最佳实践 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class Test { private UnityEngine.Ray _ray; public ref readonly UnityEngine.Ray ray => ref _ray; void Calc () { ref UnityEngine.Ray ray1 = ref ray; Calc1(ref ray1); } void Calc1 (ref UnityEngine.Ray ray2 ) { } }
⚠️ 警告 :ref return 一定不要返回局部变量的引用!
六、泛型与装箱优化 6.1 object 参数 vs 泛型 1 2 3 4 5 void Func (object o ) { }void Func <T >(T o ) { }
6.2 IL2CPP 泛型共享机制
类型
泛型共享
说明
引用类型
✅ 共享
共享 RuntimeObject
整数/枚举
✅ 共享
共享 int32_t
其他值类型
❌ 不共享
每个类型单独生成
七、可变参数优化 7.1 params 的性能问题 1 2 3 4 void Func (params int [] n ) ;Func(1 , 2 , 3 ); Func();
7.2 重载优化方案 1 2 3 4 5 public static string Format (string format, object arg0 ) ;public static string Format (string format, object arg0, object arg1 ) ;public static string Format (string format, object arg0, object arg1, object arg2 ) ;public static string Format (string format, params object [] args ) ;
八、条件编译优化 8.1 Debug.Log 的 GC 问题 1 2 3 Debug.Log(123 ); Debug.Log(string .Format("12{0}" , 3 ));
8.2 Conditional 特性 1 2 3 4 5 6 7 8 9 10 [Conditional("UNITY_EDITOR" ) ] public static void Print (object message ){ Debug.Log(message); } Print(123 ); Print(string .Format("12{0}" , 3 ));
九、数组内存布局与缓存 9.1 引用类型数组 1 2 3 4 5 6 7 8 9 引用类型数组 (不连续): ┌────┬────┬────┬────┐ │Ref1│Ref2│Ref3│Ref4│ ─┐ └────┴────┴────┴────┘ │ 引用 │ │ │ │ │ (连续) ▼ ▼ ▼ ▼ ┘ Obj Obj Obj Obj ─→ 数据 (不连续) 缓存命中率: 低
9.2 值类型数组 1 2 3 4 5 6 7 值类型数组 (连续): ┌────┬────┬────┬────┬────┬────┐ │ D1 │ D2 │ D3 │ D4 │ D5 │ D6 │ └────┴────┴────┴────┴────┴────┘ 数据 (连续内存) 缓存命中率: 高
十、容器数据结构 10.1 List/Stack/Queue 扩容机制 1 2 3 4 5 6 7 if (Count + 1 > array.Length){ var newArray = new T[array.Length * 2 ]; Array.Copy(array, 0 , newArray, array.Length); array = newArray; }
容器
扩容策略
建议
List
2倍
预设 Capacity
Stack
2倍
预设 Capacity
Queue
2倍
预设 Capacity
Dictionary
>2倍的素数
预设 Capacity
HashSet
>2倍的素数
预设 Capacity
10.2 LinkedList 内存特性 1 2 3 4 5 6 LinkedList 内存布局: ┌────┐ ┌────┐ ┌────┐ │Node│───>│Node│───>│Node│ └────┘ └────┘ └────┘ ▲ ▼ ▼ └──────────┘ 不连续内存
完全不连续内存
使用节点池可减少 GC
适合频繁插入/删除场景
十一、容器装箱问题 11.1 常见装箱场景
场景
是否装箱
解决方案
foreach (Unity 5.6+)
❌ 已修复
无需处理
接口参数 (ICollection)
✅ 会装箱
避免使用
Dictionary 结构体 Key
✅ 会装箱
实现 IEqualityComparer
Dictionary 枚举 Key
.NET 4.x+ 已修复
升级 runtime
11.2 LINQ 注意事项
⚠️ 重要 :Linq 要避免使用,除非完全理解其源码!
十二、对象池实现 12.1 UGUI 通用对象池 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 class ObjectPool <T > where T : new (){ private readonly Stack<T> m_Stack = new Stack<T>(); private readonly UnityAction<T> m_ActionOnGet; private readonly UnityAction<T> m_ActionOnRelease; public int countAll { get ; private set ; } public int countActive => countAll - countInactive; public int countInactive => m_Stack.Count; public ObjectPool (UnityAction<T> actionOnGet, UnityAction<T> actionOnRelease ) { m_ActionOnGet = actionOnGet; m_ActionOnRelease = actionOnRelease; } public T Get () { T element; if (m_Stack.Count == 0 ) { element = new T(); countAll++; } else { element = m_Stack.Pop(); } m_ActionOnGet?.Invoke(element); return element; } public void Release (T element ) { if (m_Stack.Count > 0 && ReferenceEquals(m_Stack.Peek(), element)) Debug.LogError("Trying to destroy object that is already released." ); m_ActionOnRelease?.Invoke(element); m_Stack.Push(element); } }
12.2 NGUI BetterListPool 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class BetterListPool <T >{ public const int MAX_COUNT = 16 ; PoolStack<T[]>[] pool = new PoolStack<T[]>[MAX_COUNT]; public T[] Alloc (int n ) { n = NextPowerOfTwo(n); int pos = GetSlot(n); if (pos >= 0 && pos < MAX_COUNT) { PoolStack<T[]> list = pool[pos]; if (list.size > 0 ) return list.Pop(); } return new T[n]; } public void Collect (T[] buffer ) { int pos = GetSlot(buffer.Length); if (pos >= 0 && pos < MAX_COUNT) pool[pos].Push(buffer); } }
十三、UGUI ListPool 13.1 BaseMeshEffect 优化 1 2 3 4 5 6 7 8 9 10 11 12 public override void ModifyMesh (VertexHelper vh ){ if (!IsActive()) return ; var verts = ListPool<UIVertex>.Get(); vh.GetUIVertexStream(verts); vh.Clear(); vh.AddUIVertexTriangleStream(verts); ListPool<UIVertex>.Release(verts); }
13.2 Text 特殊处理
⚠️ 注意 :UIVertex 大小 76 字节,Text 字数多时会撑大缓存。
1 2 3 4 vh.PopulateUIVertex(ref vert, index); vh.SetUIVertex(vert, index); vh.AddVert(vert);
十四、LinkedList 节点池 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class PooledLinkedList <T > : LinkedList <T >{ static Stack<LinkedListNode<T>> s_pool = new Stack<LinkedListNode<T>>(10 ); private LinkedListNode<T> Create (T t ) { LinkedListNode<T> node = s_pool.Count > 0 ? s_pool.Pop() : new LinkedListNode<T>(t); if (s_pool.Count > 0 ) node.Value = t; return node; } public new void Remove (LinkedListNode<T> n ) { int count = Count; base .Remove(n); if (count != Count) s_pool.Push(n); } }
十五、字符串优化 15.1 字符串不可变问题 1 2 3 4 5 6 str = "a" + "b" + "c" ; StringBuilder sb = new StringBuilder(); sb.Append("a" ).Append("b" ).Append("c" );
15.2 String Intern 池
类型
说明
内置池
编译期字符串,无法清空
自定义池
运行期字符串,可按需清空
15.3 字符串分类处理
类型
示例
缓存策略
整数型
“12s”
常驻池
固定拼接
“gun_ak47”
常驻池
动态内容
“张三击杀了李四”
战斗池
十六、匿名方法 16.1 GC Alloc 场景 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void Func (){ int a = 1 ; Call(() => a = 2 ); } static int a = 1 ;void Func (){ Call(() => a = 2 ); } int a = 1 ;Action action; void Func (){ if (action == null ) action = () => a = 2 ; Call(action); }
⚠️ 注意 :IL2CPP 中所有匿名形式都会产生 GC Alloc!
十七、协程优化 17.1 WaitForSeconds 缓存 1 2 3 4 5 6 yield return new WaitForSeconds (5 ) ;static readonly WaitForSeconds fiveSeconds = new WaitForSeconds(5 );yield return fiveSeconds;
17.2 自定义 IEnumerator 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Coroutine1 : IEnumerator { public bool MoveNext () { ... } public void Reset () { ... } public object Current { get { ... } } } Coroutine1 coroutine1 = new Coroutine1(); while (true ){ yield return coroutine1; coroutine1.Reset(); }
十八、Unity API 优化 18.1 避免的 API
API
问题
替代方案
object.name
产生 GC
缓存名字
object.tag
产生 GC
CompareTag()
返回数组的 API
产生新数组
使用 List 版本
18.2 List 模式 1 2 3 4 5 List<Text> texts = ListPool<Text>.Get(); GetComponentsInChildren(texts); ListPool<Text>.Release(texts);
18.3 NavMesh 优化 1 2 3 4 5 6 7 8 9 10 11 public static Vector3[] cachedPath = new Vector3[256 ];public static int pathCount { get ; private set ; }private static NavMeshPath navMeshPath;public static void CalculatePath (Vector3 startPos, Vector3 endPos ){ navMeshPath.ClearCorners(); NavMesh.CalculatePath(startPos, endPos, NavMesh.AllAreas, navMeshPath); pathCount = navMeshPath.GetCornersNonAlloc(cachedPath); }
18.4 物理 NonAlloc 1 2 3 RaycastHit[] hits = new RaycastHit[10 ]; int hitCount = Physics.RaycastNonAlloc(ray, hits);
十九、Protobuf 优化 19.1 避免流复制 1 2 3 4 5 message.MergeFrom(stream); message.MergeFrom(cachedBytes);
19.2 消息对象池 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class NetMsgInStream { public CodedInputStream codedStream { get ; private set ; } public MemoryStream memoryStream { get ; } = new MemoryStream(); public NetMsgInStream () { this .codedStream = new CodedInputStream(memoryStream); } public static ObjectPool<NetMsgInStream> pool = new ObjectPool(actionOnRelease: (s) => s.memoryStream.SetLength(0 )); } var stream = NetMsgInStream.pool.Get();message.MergeFrom(stream.codedStream); NetMsgInStream.pool.Release(stream);
二十、Unsafe 优化 20.1 字符串 ToLower 1 2 3 4 5 6 7 8 9 10 11 12 public static void ToLower (string str ){ fixed (char * c = str) { int length = str.Length; for (int i = 0 ; i < length; ++i) { c[i] = char .ToLower(c[i]); } } }
20.2 字符串 Split 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public static int Split (string str, char split, string [] toFill ){ if (str.Length == 0 ) { toFill[0 ] = string .Empty; return 1 ; } fixed (char * p = str) { var start = 0 ; int ret = 0 ; for (int i = 0 ; i < str.Length; ++i) { if (p[i] == split) { toFill[ret++] = new string (p, start, i - start); start = i + 1 ; } } if (start < str.Length) { toFill[ret++] = new string (p, start, str.Length - start); } return ret; } }
20.3 修改字符串长度 1 2 3 4 5 6 7 8 9 10 public static void SetLength (this string str, int length ){ fixed (char * s = str) { int * ptr = (int *)s; ptr[-1 ] = length; s[length] = '\0' ; } }
二十一、非托管堆 21.1 UnsafeList 实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public unsafe struct UnsafeList<T> where T : unmanaged { static int alignment = UnsafeUtility.AlignOf<T>(); static int elementSize = UnsafeUtility.SizeOf<T>(); const int MIN_SIZE = 4 ; ArrayInfo* array; public UnsafeList (int capacity ) { capacity = Mathf.Max(MIN_SIZE, capacity); array = (ArrayInfo*)UnsafeUtility.Malloc( UnsafeUtility.SizeOf<ArrayInfo>(), UnsafeUtility.AlignOf<ArrayInfo>(), Allocator.Persistent); array->capacity = capacity; array->count = 0 ; array->ptr = UnsafeUtility.Malloc( elementSize * capacity, alignment, Allocator.Persistent); } public void Dispose () { UnsafeUtility.Free(array->ptr, Allocator.Persistent); UnsafeUtility.Free(array, Allocator.Persistent); } public void Add (T t ) { EnsureCapacity(array->count + 1 ); *((T*)array->ptr + array->count) = t; ++array->count; } } unsafe struct ArrayInfo{ public int count; public int capacity; public void * ptr; }
21.2 Allocator 类型
Allocator
生命周期
性能
用途
Temp
一帧
快
临时数据
TempJob
4帧
较快
Job System
Persistent
手动释放
较慢
长期数据
二十二、stackalloc 与 Span 22.1 stackalloc 1 2 3 4 5 6 void Calculate (){ Vector3* s = stackalloc Vector3[10 ]; }
22.2 Span vs Memory
类型
特性
限制
Span<T>
ref struct,不能作字段
不能跨越 yield/await
Memory<T>
可作字段
需要转换
1 2 3 Span<int > stackSpan = stackalloc int [10 ]; Span<int > managedSpan = new int [10 ].AsSpan();
二十三、优化总结 23.1 快速检查清单
检查项
说明
✅ 缓存组件
缓存 GetComponent 结果
✅ 避免 string
使用 StringBuilder 或缓存
✅ 避免装箱
使用泛型、避免 object 参数
✅ 对象池
复用频繁创建的对象
✅ 预分配容器
设置 List/Dictionary 初始容量
✅ 使用 NonAlloc API
避免数组分配
✅ 缓存 WaitForSeconds
静态只读字段
✅ 避免匿名方法
IL2CPP 会产生 GC
23.2 工具推荐
工具
用途
Unity Profiler
查看 GC Alloc
Deep Profiler
详细调用堆栈
Memory Profiler
内存快照对比
二十四、参考资料
转载请注明来源 ,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1487842110@qq.com