GC是高并发C#应用的隐性瓶颈,尤其在Gen 0频繁满、对象晋升Gen 2或进入LOH时引发STW暂停,拖垮吞吐与P99延迟;需用ArrayPool、struct、Span等手段减少分配,并基于压测数据精准优化。
GC 是高并发 C# 应用的隐性瓶颈,不是“会不会卡”,而是“什么时候卡、卡多狠”——尤其在 Gen 0 频繁满、对象快速晋升到 Gen 2 或 LOH(大对象堆)时,STW(Stop-The-World)暂停会直接拖垮吞吐和 P99 延迟。
为什么高并发下 GC 压力特别大
高并发场景(如每秒数万请求的 Web API 或实时行情服务)往往伴随高频对象创建:临时 List、string 拼接、JSON 序列化缓冲区、DTO 实例等。这些对象多数“朝生夕死”,本该在 Gen 0 快速回收,但一旦分配速率超过 GC 回收节奏,就会引发:
- Gen 0 频繁触发(毫秒级暂停叠加成可观延迟)
- 短生命周期对象意外晋升到 Gen 1/Gen 2(比如因引用逃逸或池未复用)
- 大量 ≥85,000 字节的对象落入 LOH → 清理后不压缩 → 内存碎片 + 长时间 Full GC 风险
- 线程池线程被 GC 暂停阻塞 → 请求堆积 → 进一步加剧 GC 压力(恶性循环)
用 ArrayPool 和 MemoryPool 替代 new byte[] / new T[n]
这是最立竿见影的优化点。每次 new byte[4096] 都是堆分配;而池化能复用缓冲区,避免 Gen 0 泛滥。
var pool = ArrayPool.Shared;
byte[] buffer = pool.Rent(8192); // 复用已有数组,无分配
try
{
// 使用 buffer...
}
finally
{
pool.Return(buffer); // 归还,不保证清零!需手动 Array.Clear() 或用 Span 安全写入
}
- 默认
ArrayPool.Shared已线程安全,适合大多数场景 - 归还前务必清空敏感数据(如密码、token),
pool.Return(buffer, clearArray: true)可选 - 不要对同一块 buffer 多次
Return(),也不要在归还后继续读写 - 若需自定义大小策略或最大容量,用
ArrayPool.Create(minimumBufferSize, maximumRetainedBuffers)
用 struct 替代小 class,避开堆分配
不是所有“对象”都该是 class。坐标、范围、简单 DTO 等轻量数据结构,用 struct 能彻底消除 GC 压力,且栈分配/拷贝成本极低。