.NET 6+ 的 GC Stop-the-World 并未完全消除暂停,关键阶段(如 Gen2/LOH 回收)仍会冻结所有托管线程,实测暂停达 10–50ms;高并发加剧其破坏性,async/await 无法绕过,需通过 tracing 定位、对象复用、禁用 LOH 压缩、合理选配 GC 模式等缓解。
不是完全停,但关键阶段仍会暂停所有托管线程——尤其是 Gen2 或 LOH(大对象堆)回收时。.NET 6 引入的 Concurrent GC(默认启用)把大部分标记工作移到后台线程,但仍有短暂停顿:比如 STW 阶段需冻结线程以拍摄堆快照、重定位对象指针、更新句柄表。实测中,一次 Gen2 回收可能带来 10–50ms 的暂停,对延迟敏感服务(如金融报价、实时游戏同步)已足够触发超时。
高并发本身不直接导致 GC 更频繁,但会加剧 STW 的破坏性:
ThreadPool 线程在 STW 期间无法响应新请求,积压的 Task 延迟升高,引发级联超时string、Dictionary)推高 Gen0 分配率,间接增加 Gen2 升级概率async/await 并不能绕过 STW:await 之后的 continuation 仍需在 GC 后恢复执行,若此时刚结束 STW,调度延迟叠加Serilog 启用 Enrichers)或监控 SDK(如 OpenTelemetry)在每次调用中分配临时对象,成为隐式 GC 压力源别只看 GC 次数,重点观察暂停时长分布与请求 P99 延迟的相关性:
dot
net-trace collect --providers Microsoft-Windows-DotNETRuntime:4:4 抓取运行时事件,过滤 GCSuspendEEStart / GCSuspendEEEnd 计算实际暂停时间LOH 压缩(System.GC.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce)可减少 Gen2 停顿,但仅适用于 .NET 5+ArrayPool.Shared.Rent() 替代 new byte[100_000],防止意外落入 LOHValueStringBuilder 替代 string.Format,用 Span 解析而非 Split()
var sb = new ValueStringBuilder(stackalloc char[256]);
sb.Append("user_id:");
sb.Append(userId);
var key = sb.ToString(); // 不触发堆分配
sb.Dispose();
很多人以为“Server GC = 高并发首选”,但忽略了一个关键前提:它默认启用 Concurrent GC,却也默认开启 RetainVM(保留已释放内存),导致 RSS 持续增长。在容器环境(如 Kubernetes)中,这可能触发 OOMKilled。更糟的是,当内存压力突增时,Server GC 会尝试一次完*部回收,反而拉长单次 STW。
RetainVM:true + false
Workstation GC + Concurrent 组合:它暂停更短、更频繁,P99 延迟反而更平稳GCSettings.LatencyMode = GCLatencyMode.LowLatency 仅在极短窗口(秒级)有效,且会禁用 Gen2 回收,切勿长期启用