c# LOH 大对象堆和高并发性能的关系

7次阅读

LOH(Large Object Heap)是.net中存放≥85,000字节对象的内存区域,因不参与GC压缩而易碎片化;高并发下频繁分配大对象会触发full GC,导致线程暂停、吞吐下降。

c# LOH 大对象堆和高并发性能的关系

LOH 是什么,为什么高并发下它会成为瓶颈

LOH(Large Object Heap)是 .NET 中专门存放 ≥ 85,000 字节对象的内存区域。它不参与常规 GC 的 compact 阶段,意味着一旦分配,即使后续被回收,留下的碎片也不会被自动整理。在高并发场景下,频繁创建大对象(如 byte[]String、大型 DTO 序列化结果)会导致 LOH 快速膨胀、碎片加剧,最终触发 full GC(即 blocking GC),暂停所有线程 —— 这就是吞吐骤降、延迟毛刺的常见根源。

哪些操作会在高并发中意外触发 LOH 分配

很多看似“轻量”的操作,在高并发放大后会悄悄落入 LOH。关键不是“你写了 new byte[100000]”,而是框架/库在背后替你干了这事:

  • HttpClient.GetStringAsync() 返回的 string 如果响应体超 85KB,字符串本身进 LOH(UTF-16 编码,实际阈值 ≈ 42,500 字符)
  • jsonSerializer.Serialize(obj) 输出的 stringbyte[] 容易越界,尤其嵌套深、字段多的对象
  • Memorystream.ToArray() —— 每次调用都复制整个缓冲区到新 byte[],极易命中 LOH
  • ASP.NET Core 中 ActionResult 自动序列化返回值,T 若含大集合或长文本,风险极高

怎么查证你的服务正在被 LOH 拖累

别猜,用工具看真实行为。最直接的方式是启用 .NET 运行时事件追踪:

dotnet-trace collect --process-id  --providers microsoft-DotNETCore-EventPipe::0x1000000000000000:4

然后用 dotnet-counters monitor -p 观察关键指标:

  • LOH Size:持续增长且不回落 → 内存泄漏或碎片
  • % Time in GC > 5% 且伴随 Gen 2 GC Count 频繁上升 → LOH 触发 full GC
  • Alloc Rate / sec 突增时,若 LOH Alloc Rate / sec 同步飙升 → 确认是大对象主导分配

注意:.NET 6+ 默认启用 GCHeapHardLimit 和 LOH 压缩(需显式开启),但压缩本身有开销,不能无脑打开。

真正有效的缓解策略,不是避免大对象,而是绕过它

高并发服务的关键不是“不分配大对象”,而是“不让大对象生命周期和请求强绑定”。实操上优先考虑流式处理与池化:

  • Stream 替代 byte[]:比如 jsonSerializer.SerializeAsync(stream, obj) 直接写入响应流,不缓存完整 payload
  • 复用 ArrayPool.Shared.Rent() 分配缓冲区,严格 .Return(),避免每次 new
  • 对高频大字符串场景,改用 ReadOnlyMemory + Span 解析,跳过 string 实例化
  • ASP.NET Core 中启用 ResponseCompression 可显著减小传输体积,间接降低序列化后对象大小

LOH 问题从来不是孤立的内存问题,它是并发模型、序列化方式、IO 路径共同作用的结果。最危险的不是某次分配,而是把 LOH 分配藏在中间件、过滤器或通用泛型方法里 —— 看似安全,压测一跑就崩。

text=ZqhQzanResources