你的API latency 曲线平得像机场跑道,突然某个整点飙出200ms尖刺。监控面板里,GC(垃圾回收)的CPU占用把业务逻辑按在地上摩擦——这种剧本,.NET 团队里至少一半人见过。
问题不是GC坏了,是你把它当全自动扫地机器人,往地上狂扔垃圾。
多数开发者对GC的认知停留在"它负责清内存"。但写高并发API时,这种黑盒思维直接变成性能杀手。GC在.NET里是分代(generational)架构:对象按存活时间进三代——Gen 0像便利店,Gen 1像社区超市,Gen 2像区域仓库。越往后,扫描成本指数级上涨。
Gen 0:短命对象的屠宰场
临时字符串、LINQ中间结果、async/await的状态机——这些Gen 0常客本来该被快速清理。但当你的代码在循环里不断new对象,GC的触发频率会从"偶尔打扫"变成"追着屁股收垃圾"。
一个典型陷阱:日志字符串拼接。string.Format或插值字符串$"user={id}"每次执行都造新对象。高QPS接口里,这相当于往Gen 0倾泻小型垃圾山。更隐蔽的是ORM的跟踪查询(tracking queries),EF Core默认会把实体塞进上下文缓存,短生命周期的对象被意外续命到Gen 1甚至Gen 2。
Gen 2:大对象的沼泽地
超过85KB的对象直接进LOH(大对象堆),绕过Gen 0/1。这里不压缩、只清理,碎片化后内存占用虚高。JSON序列化大响应、文件流读取、Bitmap操作——这些场景如果反复分配,LOH会像漏气的轮胎,看起来满了,实际全是空洞。
某电商平台的订单导出接口曾因此暴雷:每次生成Excel都new一个MemoryStream,LOH碎片化导致Full GC(全代回收)每30秒触发一次,P99延迟从50ms崩到800ms。修复方案简单到尴尬——池化MemoryStream,复用而非重建。
和GC谈判的技巧
不是让你手写内存管理,而是减少无意义的分配压力。ArrayPool租还数组代替new,StringBuilder池化重用,Span和Memory在栈上切片——这些API设计初衷就是让你"少麻烦GC"。
监控层面,dotnet-counters看GC Heap Size和Gen 2 GC Count,dotnet-trace抓分配热点。指标不会撒谎:如果Gen 2回收频率超过每分钟一次,你的代码大概率在批量生产"长寿垃圾"。
那个200ms的延迟尖刺,现在你知道该查什么了——但你的监控告警规则里,有GC相关的阈值吗?
热门跟贴