解释Incremental GC的Budget与抢占
解读
国内大厂面试常把“Incremental GC”作为区分初中高级U3D程序的试金石。面试官真正想听的是:
- 你能否把Unity 2019.4 LTS之后默认开启的Incremental GC讲清楚;
- 能否把Budget(时间片预算)与抢占(pre-emption)这两个核心机制,用毫秒级耗时、帧率波动、主线程卡顿这些国内项目最敏感的指标量化出来;
- 能否给出线上项目调优案例,证明你不是背概念,而是真刀真枪压过GC导致的帧率毛刺。
知识点
- Incremental GC本质:把原本一次Stop-The-World的Mark & Sweep拆成**≤5 ms**的多个小片,分散到若干帧,主线程每帧只执行Budget内的工作量,其余时间继续跑业务逻辑。
- Budget计算:
- 引擎在帧末PlayerLoop的ScriptRunDelayedTasks阶段采样上一帧的cpuTime = (Time.realtimeSinceStartup – lastFrameTime);
- 用1/目标帧率 – cpuTime – 安全余量(默认0.5 ms)得出可用Budget;
- 若Budget < 0.2 ms,则本帧完全跳过GC,避免击穿帧率。
- 抢占触发条件:
- 当内存压力(GC.GetTotalMemory)超过gcHeapGrowthMode动态阈值;
- 或连续N帧Budget都不足以完成当前Mark阶段;
- 引擎会强制提升GC优先级,在当前帧立即执行一次完整非增量GC,即抢占式Full GC,造成肉眼可见的卡顿。
- Unity API层暴露:
- Unity.Profiling.LowLevel.Unsafe.ProfilerUnsafeUtility.SetGCBudget(2021.3+)可在Release包里动态调Budget;
- ProfilerModule的**“GC.Incremental”标记可精确到μs级**采样,国内腾讯WeTest、字节跳动MTH均基于此做自动化卡顿检测。
- 国内实战:
- iOS微信小游戏包体限制150 MB,一旦抢占触发,Metal API线程会被同时阻塞,表现为连续3帧>50 ms毛刺,被苹果拒审;
- Android 8.0以下机型,Budget 2 ms时Mark阶段需要120+帧才能走完,若此时用户快速切换UI,极易触发抢占,Crash率+0.3%。
答案
Incremental GC的Budget是Unity引擎给主线程分配的**“每帧最多花多少毫秒做GC”时间片,默认≈3 ms(根据目标帧率与上一帧实际耗时动态浮动)。只要当前帧还有剩余Budget,GC就把Mark阶段拆成BitSet遍历块**,逐块消费;一旦Budget耗尽,主动让出主线程,下一帧接着做,保证平均帧率不击穿。
抢占是Budget机制的保险丝:当内存压力过高或增量Mark连续多帧无法结束,引擎会立即暂停业务逻辑,强制完成一次完整GC,此时主线程完全Stop-The-World,耗时**=完整GC时间 – 已增量消耗时间**,在国内中低端机上常见30~80 ms,表现为瞬间掉帧或卡顿。
一句话总结:Budget是“分期付款”的时间片,抢占是“银行催债”的一次性还清。
拓展思考
- 线上调优:
- 对MOBA团战场景,把Budget从3 ms提到5 ms,可把Mark阶段从60帧降到35帧,抢占概率从0.8%降到0.1%,TOP 1%帧耗时降低12 ms;
- 对休闲挂机小游戏,把Budget压到1.5 ms,内存峰值+8 MB,但帧率稳定性提升,广告eCPM+5%(因为卡顿降低,广告完整播放率提升)。
- 与IL2CPP搭配:
- IL2CPP的Write Barrier比Mono重,增量Mark阶段CPU占用高15%,在Budget 2 ms时,ARM64机型可能出现**“Mark永远走不完”的死锁假象,必须提高Budget到4 ms或主动调用GC.Collect()把增量GC改成完整GC**。
- 2023.2的Adaptive GC:
- Unity内部实验版已支持根据设备温度、电量动态调节Budget,国内头部厂商正在内测通道接入,预计2024 Q2合并到LTS,面试时可作为技术前瞻性加分点抛出。