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<byte>.Shared.Rent()</byte>,若租借后未及时
Return(),仍会退化为 LOH 分配 .NET 6+ 默认启用
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce,但它只在下次 full GC 时生效一次,不自动周期执行
避免 LOH 分配的实操策略
核心思路是「不让大对象落到 LOH」,而非等它碎了再整理。
拆分大数组:比如处理 1MB 日志缓冲区,改用List<arraysegment>></arraysegment>+ 多个 64KB
byte[]( 优先复用:对固定尺寸大对象(如图像帧、Protobuf 序列化缓冲区),用
ArrayPool<t>.Shared</t>并严格配对
Rent()/
Return();注意
Return()传
clearArray: true可避免敏感数据残留,但有轻微性能开销 禁用 LOH 分配(仅限 .NET 5+):启动时设置环境变量
DOTNET_gcAllowVeryLargeObjects=0,强制 >85KB 对象抛
OutOfMemoryException,倒逼代码提前暴露问题
var buffer = ArrayPool<byte>.Shared.Rent(1024 * 1024); // 1MB → 仍进 LOH!
try {
// 实际使用
} finally {
ArrayPool<byte>.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 <pid></pid>抓快照,加载到 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 <pid> --counters System.Runtime</pid>实时观察
gc-loh-size和
gc-gen-2-collect-count,比盲目改配置更可靠
LOH 碎片化本质是内存使用模式和 GC 行为不匹配的结果。最常被忽略的是:开发阶段没压测真实数据体积,上线后突发大 payload(如上传 200MB Excel)直接打穿 LOH,此时再加 compaction mode 已晚。把大对象生命周期纳入接口契约(比如明确要求调用方分块上传),比依赖运行时补救更有效。
