LOH碎片化会因无法分配连续内存而触发full GC,导致高并发下Stop-The-World并拖垮吞吐量;其根源是LOH不自动压缩,需通过避免大对象分配、及时归还ArrayPool缓冲区、分块处理等手段预防。

LOH 碎片化如何影响高并发吞吐量
当 byte[]、string 或大型自定义对象(≥ 85,000 字节)频繁分配时,.NET 会将其放入大对象堆(LOH)。LOH 不在每次 GC 时压缩 —— 这是关键。碎片化后,即使总空闲空间足够,也可能无法满足下一个大对象的连续内存请求,触发 full GC(GC.Collect(2)),而 full GC 在高并发下会 Stop-The-World,直接拖垮吞吐量。
- 典型现象:
Gen2 GC count暴涨,但LOH size没明显增长;监控中% Time in GC突增,且线程池Worker Thread starvation频发 - 不是所有大对象都“安全”:即使你用
ArrayPool,若租借后未及时.Shared.Rent() Return(),仍会退化为 LOH 分配 - .NET 6+ 默认启用
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce,但它只在下次 full GC 时生效一次,不自动周期执行
避免 LOH 分配的实操策略
核心思路是「不让大对象落到 LOH」,而非等它碎了再整理。
- 拆分大数组:比如处理 1MB 日志缓冲区,改用
List+ 多个 64KB> byte[]( - 优先复用:对固定尺寸大对象(如图像帧、Protobuf 序列化缓冲区),用
ArrayPool并严格配对.Shared Rent()/Return();注意Return()传clearArray: true可避免敏感数据残留,但有轻微性能开销 - 禁用 LOH 分配(仅限 .NET 5+):启动时设置环境变量
DOTNET_gcAllowVeryLargeObjects=0,强制 >85KB 对象抛OutOfMemoryException,倒逼代码提前暴露问题
var buffer = ArrayPool.Shared.Rent(1024 * 1024); // 1MB → 仍进 LOH! try { // 实际使用 } finally { ArrayPool .Shared.Return(buffer, clearArray: false); // 必须 return,否则池耗尽后 fallback 到 new byte[] }
诊断 LOH 碎片化的关键指标
别等服务卡顿才查 —— 直接看 GC 日志和 ETW 事件。
- 启用 GC 日志:
dotnet run --environmentVariables DOTNET_gcLog=1,关注日志中LOH segment count和LOH fragmentation字段 - 用
dotnet-gcdump collect -p抓快照,加载到 PerfView,筛选Object Type含System.Byte[]且Size≥ 85000 的实例,按大小排序看是否大量“小而散”的大数组 - Windows 性能计数器:
.NET CLR Memory\# Bytes in LOH+.NET CLR Memory\% Time in GC联动突增,基本可锁定
高并发场景下 GC 设置的取舍
服务器应用不是调低 GC 频率就万事大吉,要平衡延迟与吞吐。
- 禁用后台 GC(
GCSettings.IsServerGC = true默认开启,但需确认):服务器 GC 比工作站 GC 更适合高并发,它为每个 CPU 核心维护独立的 heap,减少锁争用 - 慎用
GC.TryStartNoGCRegion():它会在指定大小内禁止 GC,但一旦失败(如 LOH 不足),会立即触发 full GC —— 高并发下极易雪崩,仅适合已知内存上限的短时批处理 - 监控比调优重要:在 K8s 中用
dotnet-counters monitor --process-id实时观察--counters System.Runtime gc-loh-size和gc-gen-2-collect-count,比盲目改配置更可靠
LOH 碎片化本质是内存使用模式和 GC 行为不匹配的结果。最常被忽略的是:开发阶段没压测真实数据体积,上线后突发大 payload(如上传 200MB Excel)直接打穿 LOH,此时再加 compaction mode 已晚。把大对象生命周期纳入接口契约(比如明确要求调用方分块上传),比依赖运行时补救更有效。











