1.频繁内存分配 2.垃圾回收 3.分代GC 4.减少GC的方法 1.频繁内存分配 内存分配不是"免费创建对象" , 而是操作系统在底层执行复杂操作的过程, 频繁分配会直接消耗CPU资源, 破坏内存效率1 ) . 堆内存分配的底层开销( c#引用类型对象) C#中引用类型( 如string , List< T> , 自定义类) 的内存分配在"堆" 上( 值类型在栈上, 分配和释放极快) , 堆分配的核心开销 来自两方面: a. 空间块查找与管理 堆是动态内存区域, 分配时需要先查找"大小匹配的空闲内存块" , 频繁分配小对象会导致堆中产生大量零散的空闲块( 内存碎 片化) , 后续分配时需要遍历更长的空闲链表才能找到合适块, 分配耗时逐渐增加 b. 元数据与安全检查 每个堆对象都需要附加元数据( 如: 类型信息, GC标记位, 同步锁标识) , 分配时运行时还要做边界检查, 权限验证, 这些都 是额外的CPU开销2 ) . 破坏CPU缓存局部性 CPU缓存( L1/ L2/ L3) 的核心优势是"访问连续内存时命中率极高" ( 局部性原理) , 频繁分配的对象在堆中往往是离散分布的( 尤 其是碎片化后) , 导致CPU访问这些对象时, 缓存命中率大幅下降- 不得不从速度慢100 倍以上的主存中读取数据, 直接拖慢 代码执行速度( 这也是"频繁new对象" 比"复用对象" 慢的关键性原因) 3 ) . 帧时间的累积消耗 Unity的帧循环有严格的时间限制( 60 帧要求每帧<= 16ms, 30 帧<= 33ms) , 如果每帧都进行多次内存分配( 比如: 临时string 拼接, new List< int > ( ) , LINQ查询隐式创建迭代器) , 这些分配的底层开销会直接占用帧时间; 如 a. 每帧分配10 个小对象, 每个分配耗时0 . 1ms, 仅分配就占用1ms b. 若项目本身逻辑复杂( 如物理计算, UI渲染) , 再叠加分配开销, 很容易导致帧时间超标, 出现"微卡顿" 2.垃圾回收 内存分配只是"前因" , 真正导致明显卡顿的是后续的GC, 因为GC的核心工资机制是"暂停所有线程" ; Unity的主线程( 负责渲 染, 输入, 逻辑更新) 一旦被暂停, 画面就会直接卡住1 ) . GC的核心工作原理 C#的GC是"自动内存回收" , 但不是"无代价的" , 其核心流程( 以"标记 - 清除" 为例) a. 标记阶段 暂停所有应用线程( STW) , 遍历堆中所有对象, 标记出"仍被引用的存活对象" ( 如全局变量, 局部变量指向的对象) b. 清除阶段 继续STW, 回收未被标记的"垃圾对象" , 释放其占用的堆内存 c. 压缩阶段 为了解决碎片化, 将存活对象整理成连续的内存块( 同样需要STW) 2 ) . STW是卡顿的直接原因 a. 暂停主线程 Unity的主线程是"单线程驱动的" , 所有关键逻辑( Update, LateUpadte, 渲染管线, UI布局) 都在主线程中指向; GC触发时, 主线程会强制暂停- GC执行多久, 画面就卡多久 b. 触发频率与耗时 频繁内存分配会导致堆内存快速增长, GC触发频率大幅度升高- Minor GC ( 年轻代回收) : 回收新分配的短期对象( 如每帧创建的临时对象) , 耗时较短( 通常1 ~ 3ms) , 但频繁触发( 比如每1 0 帧一次) 会累积延迟, 导致帧时间波动- Major GC ( 老年代回收) : 回收长期存活的对象, 涉及整个堆的遍历和整理, 耗时更长( 移动平台可能5 ~ 20ms 甚至更久) 一 次Major GC就足以让60 帧画面掉帧( 16ms 阈值) , 出现明显卡顿 c. 不确定性 GC的触发时机是运行时动态决定的( 如堆内存达到阈值、手动调用GC. Collect ( ) ) , 可能在关键场景( 如战斗爆发、UI 切换) 突 然触发, 卡顿体验更差3.分代GC 1 ) . 分代GC的核心结构 Unity SGen GC将堆分为两个核心区域: 年轻代和老年代 a. 年轻代- 存储对象类型: 新分配的短期对象( 如临时List, 字符串) - 回收效率: 极快( ms级) - 对应回收类型: Minor GC b. 老年代- 存储对象类型: 存活超过多次Minor GC的长期对象( 如游戏单例, 场景根对象) - 回收效率: 慢( 10ms级) - 对应回收类型: Major GC/ Full GC 注: "部分大对象(如 > 256kb的数组, 纹理数据)会直接分配到老年代(年轻代存不下), 跳过年轻代阶段" 2 ) . Minor GC ( 年轻代回收) : 触发时机( 高频, 轻量) Minor GC的核心触发逻辑是: 年轻代( Nursery) 内存不足, 无法容纳新分配的对象, 这是最主要的, 最常见的触发条件, 且 几乎都是"自动, 被动" 触发 a. 核心触发条件( 99 % 的场景) 当你在代码中分配新的引用类型对象( 如new List< > ( ) 、字符串拼接) , 运行时尝试将对象放入年轻代时: 若年轻代剩余空闲空间 ≥ 新对象大小 → 正常分配, 不触发GC 若年轻代剩余空闲空间 < 新对象大小 → 立即触发MinorGC ✅ 第一步: 暂停主线程( STW) , 扫描年轻代所有对象, 标记"存活对象" ( 如仍被变量引用的临时对象) ✅ 第二步: 回收年轻代中"死亡对象" ( 无引用的垃圾) , 释放空间 ✅ 第三步: 将年轻代中"存活超过1 ~ 2次Minor GC" 的对象( 如存活了3 帧的临时对象) "晋升(Promote)" 到老年代 ✅ 第四步: 若回收后年轻代有足够空间, 分配新对象; 若仍不足( 极少) 则触发Major GC b. 辅助触发条件( 少见) 手动指定回收年轻代: 调用GC. Collect ( 0 ) 参数0 代表年轻代, 强制触发Minor GC 运行时内部阈值: 极少数情况( 如年轻代碎片化达到临界值) , 运行时主动触发Minor GC c. Unity实战中的Minor GC典型场景 每帧创建临时对象( 如string a= "hp:" + playerHp; 、new Vector3 ( ) 作为返回值) , 年轻代快速被填满, 通常每10 ~ 30 帧触发一次 Minor GC 战斗场景中频繁创建子弹/ 粒子临时对象, 年轻代5 ~ 10 帧就满, Minor GC触发频率飙升 注: Minor GC仅扫描年轻代( 内存范围小) , 因此STW耗时极短( 移动平台0.5 ~ 3ms, PC0.1 ~ 1ms) , 频繁触发会累积帧时 间波动3 ) . Major GC ( 老年代回收) / Full GC ( 全堆回收, 包含年轻代+ 老年代+ 压缩) 的触发条件更复杂, 核心是"老年代内存不足" 或"全局内存压力达到临界值" , 且常与Minor GC联动 a. 核心触发条件: 老年代内存不足- 直接触发: 分配大对象( 如: 1MB以上的数组、游戏场景数据) 时, 大对象直接进入老年代, 若老年代剩余空间不足 → 触发Major GC ( 先回收老年代垃圾, 再分配) - 阈值触发: 老年代已用内存占老年代总容量的比例达到阈值( Unity SGen 默认约75 % ~ 80 % ,不同版本/ 平台略有调整) → 触发Major GC- 碎片化触发: 老年代碎片化严重( 有大量空闲内存, 但无连续大块空间容纳新对象) → 触发Full GC ( 含压缩阶段, 整理内 存碎片) b. 间接触发: Minor GC的"晋升失败" 这是Major GC最常见的"间接触发" 场景: Minor GC执行后, 需要将年轻代中"存活多次的对象" 晋升到老年代, 但此时老年代 没有足够空间容纳这些晋升对象 → 触发"晋升失败(Promotion Failure)" → 运行时会先触发Major GC清理老年代空间, 再 完成年轻代对象的晋升 c. 手动触发( 主动控制/ 风险操作) - 调用GC. Collect ( ) : 无参数, 强制触发Full GC ( 回收年轻代+ 老年代,且可能执行内存压缩) - 调用GC. Collect ( 1 ) : 参数1 代表老年代强制触发Major GC- Unity特定操作: 手动调用Resources. UnloadUnusedAssets ( ) 卸载未使用资源后, 通常会触发Full GC清理资源对应的内存 对象 d. 系统/ 运行时强制触发- 低内存压力: 移动平台( iOS/ Android) 触发"低内存警告" , 系统强制Unity回收内存 → 运行时触发Full GC- 堆内存耗尽: 整个堆( 年轻代+ 老年代) 的空闲内存不足以分配新对象, 且Minor/ Major GC都无法释放足够空间 → 触发Full GC ( 最后尝试回收,失败则抛出 OOM 内存溢出) - Unity场景卸载: 场景切换时, 大量长期对象( 如场景内的管理器、模型对象) 变为垃圾, 老年代占比骤升 → 触发Major GC e. Unity实战中的Major GC典型场景 场景切换: 卸载旧场景后, 大量长期对象进入老年代垃圾, 触发Major GC ( 移动平台耗时5 ~ 20ms) 战斗结束: 大量战斗相关长期对象( 如技能管理器、敌人对象) 失效, 老年代占比达阈值 → 触发 Major GC 频繁创建大对象: 如每帧加载1MB的配置数据数组( 直接进老年代) , 老年代快速填满 → 触发 Major GC 注: Major/ Full GC需要扫描老年代( 内存范围大) , 且可能执行内存压缩, STW耗时远高于Minor GC ( 移动平台5 ~ 20ms, PC3 ~ 10ms) , 一次就可能导致明显卡顿4.减少GC的方法 1 ) . 避免在Update中创建对象// 错误示例:在Update中创建字符串 void Update ( ) { string message= "Current time: " + Time. time; // 每帧创建新字符串 Debug. Log ( message) ; } // 正确示例:预分配字符串 private string messageBuffer= "" ; void Update ( ) { messageBuffer= string . Format ( "Current time: {0}" , Time. time) ; // 复用字符串缓冲区 Debug. Log ( messageBuffer) ; } 2 ) . 使用StringBuilder处理字符串拼接// 错误示例:频繁的字符串拼接 string fullName= firstName+ " " + lastName+ " - Age: " + age; // 正确示例:使用StringBuilder StringBuilder sb= new StringBuilder ( ) ; sb. Append ( firstName) . Append ( " " ) . Append ( lastName) . Append ( " - Age: " ) . Append ( age) ; string fullName= sb. ToString ( ) ; 3 ) . 避免装箱和拆箱操作// 错误示例:int装箱 object boxedInt= 10 ; int unboxedInt= ( int ) boxedInt; // 正确示例:使用泛型避免装箱 List< int > intList= new List< int > ( ) ; intList. Add ( 10 ) ; 4 ) . 游戏对象池的使用using System. Collections. Generic ; using UnityEngine ; public class ObjectPool : MonoBehaviour { [ SerializeField ] private GameObject pooledObject; [ SerializeField ] private int poolSize= 10 ; private List< GameObject> objectPool; void Start ( ) { objectPool= new List< GameObject> ( ) ; for ( int i= 0 ; i< poolSize; i++ ) { GameObject obj= Instantiate ( pooledObject) ; obj. SetActive ( false ) ; objectPool. Add ( obj) ; } } public GameObject GetPooledObject ( ) { foreach ( GameObject objin objectPool) { if ( ! obj. activeInHierarchy) { obj. SetActive ( true ) ; return obj; } } // 如果池已满,可以创建新对象或扩展池 GameObject newObj= Instantiate ( pooledObject) ; objectPool. Add ( newObj) ; return newObj; } public void ReturnToPool ( GameObject obj) { obj. SetActive ( false ) ; } } 5 ) . 类对象池的使用public class Pool< T> where T : class , new ( ) { private Stack< T> objectStack; private int maxSize; public Pool ( int initialSize, int maxSize) { objectStack= new Stack< T> ( ) ; this . maxSize= maxSize; for ( int i= 0 ; i< initialSize; i++ ) { objectStack. Push ( new T ( ) ) ; } } public T Get ( ) { return objectStack. Count> 0 ? objectStack. Pop ( ) : new T ( ) ; } public void Return ( T obj) { if ( objectStack. Count< maxSize) { objectStack. Push ( obj) ; } } } // 使用示例 private static Pool< List< int > > listPool= new Pool< List< int > > ( 10 , 100 ) ; void SomeMethod ( ) { List< int > list= listPool. Get ( ) ; list. Add ( 1 ) ; list. Add ( 2 ) ; // 使用完后归还 list. Clear ( ) ; listPool. Return ( list) ; } 6 ) . 预分配内存// 错误示例:每次调用都分配新数组 void ProcessData ( List< int > data) { int [ ] array= data. ToArray ( ) ; // 分配新数组 // 处理数组 } // 正确示例:预分配数组 private int [ ] tempArray= new int [ 1000 ] ; void ProcessData ( List< int > data) { data. CopyTo ( tempArray) ; // 复用预分配的数组 // 处理数组 } 7 ) . 字符串优化 a. 避免在循环中使用字符串拼接// 错误示例:在循环中拼接字符串 string result= "" ; for ( int i= 0 ; i< 1000 ; i++ ) { result+= i. ToString ( ) ; // 每次循环都创建新字符串 } // 正确示例:使用StringBuilder StringBuilder sb= new StringBuilder ( ) ; for ( int i= 0 ; i< 1000 ; i++ ) { sb. Append ( i) ; } string result= sb. ToString ( ) ; b. 缓存常用字符串// 正确示例:缓存常用字符串 private const string scorePrefix= "Score: " ; private const string healthPrefix= "Health: " ; c. 使用StringBuilder缓存( 创建管理器) d. 使用struct 代替class 定义小型数据结构 e. 减少ToString ( ) 调用// 错误示例:频繁调用ToString() Debug. Log ( "Position: " + transform. position. ToString ( ) ) ; // 正确示例:自定义日志格式 Debug. LogFormat ( "Position: ({0}, {1}, {2})" , transform. position. x, transform. position. y, transform. position. z) ;