解释Incremental GC的Budget与抢占

解读

国内大厂面试常把“Incremental GC”作为区分初中高级U3D程序的试金石。面试官真正想听的是:

  1. 你能否把Unity 2019.4 LTS之后默认开启的Incremental GC讲清楚;
  2. 能否把Budget(时间片预算)抢占(pre-emption)这两个核心机制,用毫秒级耗时、帧率波动、主线程卡顿这些国内项目最敏感的指标量化出来;
  3. 能否给出线上项目调优案例,证明你不是背概念,而是真刀真枪压过GC导致的帧率毛刺。

知识点

  1. Incremental GC本质:把原本一次Stop-The-World的Mark & Sweep拆成**≤5 ms**的多个小片,分散到若干帧,主线程每帧只执行Budget内的工作量,其余时间继续跑业务逻辑。
  2. Budget计算
    • 引擎在帧末PlayerLoop的ScriptRunDelayedTasks阶段采样上一帧的cpuTime = (Time.realtimeSinceStartup – lastFrameTime)
    • 1/目标帧率 – cpuTime – 安全余量(默认0.5 ms)得出可用Budget
    • 若Budget < 0.2 ms,则本帧完全跳过GC,避免击穿帧率。
  3. 抢占触发条件
    • 内存压力(GC.GetTotalMemory)超过gcHeapGrowthMode动态阈值;
    • 连续N帧Budget都不足以完成当前Mark阶段
    • 引擎会强制提升GC优先级,在当前帧立即执行一次完整非增量GC,即抢占式Full GC,造成肉眼可见的卡顿。
  4. Unity API层暴露
    • Unity.Profiling.LowLevel.Unsafe.ProfilerUnsafeUtility.SetGCBudget(2021.3+)可在Release包里动态调Budget;
    • ProfilerModule的**“GC.Incremental”标记可精确到μs级**采样,国内腾讯WeTest、字节跳动MTH均基于此做自动化卡顿检测。
  5. 国内实战
    • 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是“分期付款”的时间片,抢占是“银行催债”的一次性还清

拓展思考

  1. 线上调优
    • 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%(因为卡顿降低,广告完整播放率提升)。
  2. 与IL2CPP搭配
    • IL2CPP的Write Barrier比Mono重,增量Mark阶段CPU占用高15%,在Budget 2 ms时,ARM64机型可能出现**“Mark永远走不完”的死锁假象,必须提高Budget到4 ms主动调用GC.Collect()把增量GC改成完整GC**。
  3. 2023.2的Adaptive GC
    • Unity内部实验版已支持根据设备温度、电量动态调节Budget国内头部厂商正在内测通道接入,预计2024 Q2合并到LTS,面试时可作为技术前瞻性加分点抛出。