c# 大对象堆(LOH)碎片化和高并发性能下降

来源:这里教程网 时间:2026-02-21 17:40:12 作者:

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 已晚。把大对象生命周期纳入接口契约(比如明确要求调用方分块上传),比依赖运行时补救更有效。

相关推荐