如何检测Managed Heap的碎片化

解读

国内 Unity 项目普遍重度依赖 C# 脚本,一旦上线后频繁出现 GC.Collect 抖动、Mono 涨内存但 Profiler 里“Used”不高,面试官就会追问“是不是托管堆碎片化了?”
该问题考察两点:

  1. 能否把“托管堆碎片化”与“内存泄漏”“资源冗余”区分开;
  2. 能否用现成工具+代码埋点给出量化证据,而不是拍脑袋答“GC 多了就是碎片”。
    面试时,如果只说“看 GC 次数”会被判为不及格;必须给出可落地的检测步骤与数据指标

知识点

  1. 托管堆底层:Mono(或 IL2CPP)按**段(segment)向 OS 申请内存,段内按块(chunk)**划分;
  2. 碎片类型:
    • 小对象堆(SOH)碎片:< 85 kB 对象,GC 后产生空闲间隙;
    • 大对象堆(LOH)碎片:≥ 85 kB 对象,LOH 默认不压缩;
  3. 核心指标:
    • 堆利用率 = 存活对象大小 / 托管堆总大小;
    • 最大空闲块 / 平均空闲块 比值越大,碎片越严重;
  4. Unity 暴露的接口:
    • Profiler.GetTotalAllocatedMemoryLong(Used);
    • Profiler.GetTotalMonoUsedSizeLong(Mono Used);
    • Profiler.GetTotalMonoHeapSizeLong(Mono Heap);
    • 2021.2+ 新增 Profiler.EmitFrameMetaData + MonoMemorySnapshot 可抓堆布局;
  5. 工具链:
    • Unity Profiler(Module:Memory,视图:Managed Heap);
    • 手游真机:腾讯 WeTest PerfDog、字节 MallocDebugger 均可拉 Mono 段信息;
    • PC 端:JetBrains dotMemory、Microsoft CLR MD 库离线分析快照;
  6. 红线值:国内大厂上线标准——堆利用率 < 60 % 且最大空闲块 > 5 MB 即判定为碎片化,必须热更修复。

答案

“我在项目中分三步检测托管堆碎片化,既能在编辑器快速定位,也能在上线后远程报警。
第一步:
运行时通过 Profiler.GetTotalMonoHeapSizeLongGetTotalMonoUsedSizeLong 计算堆利用率,每 5 秒采样一次。若利用率 < 60 % 且连续 3 个采样点持续下降,则触发二级采样。

第二步:
二级采样调用 Unity 2021.2 提供的 MonoMemorySnapshot API,把当前托管堆所有段、块地址与占用情况写成 json 落盘。利用自研脚本统计最大空闲块、平均空闲块、碎片率(总空闲大小/堆大小)

第三步:
真机包体内置 Debug.m_ProfilerEnabled 开关,QA 复测时打开,PerfDog 拉取 Mono 段信息回传后台。后台用 CLR MD 库离线解析,输出报告:若最大空闲块 > 5 MB 且堆利用率 < 60 %,则判定碎片化超标,自动提 Jira 并@客户端负责人。

用这套流程,我们在去年 11 月的版本上线前发现 LOH 碎片 38 MB,利用率仅 42 %,通过压缩 LOH(调用 GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce 后手动 GC.Collect) 把利用率提到 78 %,GC 抖动从 37 ms 降到 11 ms,顺利过审。”

拓展思考

  1. IL2CPP 下托管堆实际是 libil2cpp 的 GarbageCollector 管理,段信息不再经过 Mono,检测时要换用 il2cpp_gc_get_heap_sizeil2cpp_gc_get_used_size(通过 P/Invoke 拉符号);
  2. 碎片化根因 90 % 是频繁实例化大数组/List(如网络包、地图块),可引入 ArrayPool<T> + 对象池 预分配 2 倍峰值,把 LOH 对象变成复用对象;
  3. Unity 2022.3 实验性支持 Moving GC(压缩式 GC),开启后 SOH 碎片可忽略,但 IL2CPP 构建体积增加 5 %–8 %,需评估包体;
  4. 若项目已上线且无法热更代码,可把大对象拆小(< 85 kB)落到 SOH,利用代际回收降低碎片;
  5. 海外 Google Play 从 2023 年起强制 Android App Bundle 64 位,Mono 堆段上限从 512 MB 提到 4 GB,碎片阈值需重新校正,避免误报。