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, PC 0.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, PC
3 ~ 10ms) , 一次就可能导致明显卡顿
4.减少GC的方法
1 ) . 避免在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 sb = new StringBuilder ( ) ;
sb. Append ( firstName) . Append ( " " ) . Append ( lastName) . Append ( " - Age: " ) . Append ( age) ;
string fullName = sb. ToString ( ) ;
3 ) . 避免装箱和拆箱操作
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 obj in 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 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. 使用struct 代替class 定义小型数据结构
d. 减少ToString ( ) 调用
Debug. Log ( "Position: " + transform. position. ToString ( ) ) ;
Debug. LogFormat ( "Position: ({0}, {1}, {2})" , transform. position. x, transform. position. y, transform. position. z) ;