资讯动态

微软用了24年,NET GC 为何走向自适应堆?

发布时间:2026/9/30 7:10:41 来源:尧图企业网站定制
.NET GC 二十四年从分代假设到自适应堆大家好我是搬砖工呀。我们上文说过.NET12可能会引入LXR超低延迟GC但那毕竟是未来进行时需要等待。我们今天先来过去完成时一起回顾下.NET GC领域的来时路欢迎大家收藏作为资料备用。从 2002 年的 .NET Framework 1.0 到 2026 年的 .NET 11垃圾回收器GC一共走过 24 个版本。永恒目标所有GC的改变其实都在回答同一个问题堆该有多大、暂停该有多短。不幸的是这两个目标天然对立。堆给得小、内存省但回收会更频繁。暂停要短就得让回收和应用并行。二十四年的演进就是在这条张力线上不断拉扯找新平衡点努力做到既要又要还要又想起了我的打工人生。先从.NET诞生说起。时代一 · 奠基分代 GC.NET Framework 1.0 – 3.52002–20091.1 分代假设只扫年轻代.NET GC 诞生时最核心的一个赌注是分代假设generational hypothesis绝大多数对象朝生夕死少数对象活得极久。既然如此就不必每次都扫全堆 —— 只扫年轻代就能回收掉大部分垃圾。堆被切分成三类gen0 / gen1合住一个短暂段ephemeral segment新对象先落到 gen0熬过一次回收才升到 gen1成为长期存活对象才升到 gen2。gen2长期存活对象区。回收 gen2 等于扫全堆因此也叫完全 GC。LOH大对象堆大小 ≥ 85,000 字节的对象直接进 LOH不经过 gen0/gen1。初代 LOH 默认只清扫、不压缩这是后来十几年碎片问题的根源。一次压缩式回收分三个阶段阶段做什么代价① 标记 Mark从根栈变量、静态字段、GC 句柄出发遍历对象图给活对象打标记遍历成本正比于存活对象数② 重定位 Relocate计算每个活对象的新地址并改写所有指向它的引用需要暂停应用否则引用会读到一半③ 压缩 Compact把活对象搬到一起空闲空间集中到尾部搬运成本换来的收益是分配只是指针加法压缩是这个设计的精髓它让分配变成几乎零成本的操作代价则是暂停期间必须搬动内存。.NET GC最早是从LISP翻译过来的。导致一个单文件上万行我滴乖乖。可以这样说.NET GC上来就是一个现代化的GC,有分代有压缩。是个标准的精准GC。而不是Mono那个线性GC,还不准确。微软一上来就给.NET配置了当时最好的技术。1.2 一天就分好的两种口味工作站 GC vs 服务器 GC同样在 1.0GC 就分裂成了两个目标截然不同的版本工作站 GC为客户端程序设计单个堆、在触发回收的用户线程上执行、优先级正常 —— 追求的是低延迟暂停短。服务器 GC为服务器程序设计每个逻辑处理器一个堆 一条专用回收线程Windows 上以THREAD_PRIORITY_HIGHEST运行多个堆同时回收 —— 追求的是吞吐。这个工作站 / 服务器的二分是后面二十四年所有调优的起点,工作站GC几乎没啥达大变化。1.3 缩短暂停的第一次尝试并发 GC分代解决了少扫但 gen2 完全 GC 依旧要停世界Stop-The-World即知名的STW时间。**并发 GCconcurrent GC**是最早的解法把 gen2 的回收挪到一条专用线程上让应用线程大部分时间照常运行。它的代价很关键也是后来被取代的原因并发回收期间应用不能随意分配。因为一旦在短暂段里分配满就必须停下来做前台回收去处理新生代对象 —— 所以并发 GC 保留了一条分配限制。另外gen0/gen1 从不并发它们太快并行反而不划算并发只作用于 gen2。gcConcurrent配置项从.NET Framework 1.1起就存在此后一直是工作站 GC 的默认行为一直沿用到 3.5。在最早的年月.NET GC一直是真正的自动挡你除了能配置工作模式外没用其他参数让你设置。甚至不能手动调用回收资源。1.4 手动GC**Framework 2.02005**补齐了一批手动干预 APIGC.Collect(generation)、GC.AddMemoryPressure、GC.GetTotalMemory。这是开发者第一次能主动影响 GC 的行为 —— 虽然手动调GC.Collect后来成了最著名的性能反模式。时代二 · 后台化把暂停和内存都做成可选项Framework 4.0 – 4.62010–20152.1 后台 GC取消分配限制**Framework 4.02010用后台 GCBackground GC, BGC**取代了并发 GC。改动看起来只是换了名字实际解决的是并发 GC 那个不能随意分配的毛病gen2 在后台标记的同时gen0/gen1 可以做短促的前台回收—— 于是分配限制被彻底取消。当然每次前台回收仍然要短暂停世界但只剩那几小段通常几毫秒级。后台线程数量工作站 1 条会超时让位服务器约每逻辑处理器 1 条不超时。Framework 4.0BGC 仅支持工作站 GC。Framework 4.52012BGC 扩展到服务器 GC并成为服务器 GC 的默认模式。2.2 接下来十年四个特性覆盖了后面所有需求的雏形版本特性解决的问题配置 / API4.0 / 4.5后台 GCgen2 不再停世界4.5 起服务器 GC 也有System.GC.Concurrent/gcConcurrent默认开4.5.1按需 LOH 压缩LOH 默认只清扫不搬运碎片多了就够不着GCSettings.LargeObjectHeapCompactionMode CompactOnce4.6NoGC Region关键路径前预留内存声明这段时间别回收避免延迟尖刺GC.TryStartNoGCRegion/GC.EndNoGCRegion4.8省内存模式宁可回收更频繁、暂停更长也要把堆压小GCConserveMemory 0…9这四个特性几乎预告了后面所有的低延迟与省内存需求按需压缩是后来 POH/区域压缩的先声NoGC Region是延迟敏感场景的标准做法ConserveMemory则是容器/多租户时代省内存旋钮的第一次出现。同期还有几个容易被忽略的改动4.5 引入LOH 空闲块复用算法并解禁 2 GB 数组4.6.2 为服务器 GC 补上亲和性三件套HeapCount/NoAffinitize/HeapAffinitizeMask—— 这三件套后来在 .NET Core 3.0 完整落地。多嘴提一句开发POH的开发者应该现在也不在搞.NET的GC了于是才有了新来者。于是我们也可能有了LXR。时代三 · 跨平台与容器化让 GC 在别人的机器上守规矩.NET Core 1.0 – 3.12016–20193.1 配置搬家.NET Core 1.02016GC 跟着运行时一起跨平台。最直观的变化是配置位置从app.config搬到runtimeconfig.jsonSystem.GC.Server/System.GC.Concurrent/System.GC.RetainVM。3.2 旋钮的爆发期如果只看有多少个可调的 GC 配置项Core 3.0 – .NET 6 是绝对的爆发期。原因很具体这几年 .NET 开始大规模跑在容器里而容器的问题不是GC 快不快而是GC 会不会把整个宿主机吃光。**.NET Core 3.02019**一次性上了三组关键能力容器成为一等公民GCHeapHardLimit/HeapHardLimitPercent让 GC 按cgroup 限额算内存而不是按宿主机物理内存 —— 这是 .NET 能在 Kubernetes 里活下去的前提。服务器 GC 亲和性全套HeapCount/NoAffinitize/HeapAffinitizeMask/HeapAffinitizeRanges后者支持 64 个处理器。堆自省GC.GetGCMemoryInfo()——应用第一次能自己看见 GC 的堆与压力这让根据内存压力做业务决策成为可能。同期LOH 阈值LOHThreshold也变成可配置项默认仍是 85,000 字节。时代四 · 可配置与分区化.NET 5 – 72020–20224.1 POH把钉住的对象单独关起来.NET 5被fixed/GCHandle钉住pinned的对象一直是压缩式 GC 的噩梦 —— 它们不能搬动于是会在堆里钉出碎片。.NET 5引入POH固定对象堆Pinned Object Heap被固定的数组与缓冲进 POH不再污染普通堆。同时SOH / LOH / POH三类堆可以分别设置硬限制与百分比。4.2 用独立 GC孵化大特性.NET 6**.NET 62021干了一件对后来影响深远的事把regions区域放进独立 GCclrgc.dll**里孵化 —— 先用实验开关放到独立 GC 里试成熟了再默认开启。这成了此后大特性regions、DATAS的标准发布惯例危险的东西先做成可插拔的。4.3 段 → 区域内存粒度从 GB 级到 MB 级.NET 7.NET 72022GC 堆的物理表示在 64 位 Windows / Linux 上从段换成区域。这是继后台 GC 之后最重要的结构性改动。段模型的问题段是个大块头服务器 GC 64 位下每段 1 / 2 / 4 GB工作站 256 MB块内被世代边界切死。gen2 或 LOH 里出现的大块空闲因为已经提交、又不在段尾别的世代想用也用不了 —— 结果是内存够用却分配不出来。只有在段尾才能 decommit而段尾一旦被 pin 住连回收地址空间都做不到。区域模型.NET 7 起把内存切成SOH 区域 4 MB、UOHLOH POH区域 32 MB。回收掉的区域不还给世代而是回收到空闲区域池—— 任何世代、甚至同一个进程里的其他堆都可以直接取用。这次改动带来两个连锁收益空闲区域可跨世代、跨堆复用decommit 可以发生在任意位置而不是只在段尾。这是后来 DATAS 能缩堆的地基—— 没有区域把堆还回去根本无从实现。代价是地址空间区域模式默认预留256 GB地址空间仅保留、不提交若物理内存超过 256 GB默认翻倍。同期同样是 .NET 7服务器 GC 在Windows 11 / Server 2022上默认使用全部 CPU 组 64 核的机器不再被单个处理器组限制。时代五 · 自适应把最难的决定交回运行时.NET 8 – 102023–20265.1 DATAS 登场让堆跟着数据走.NET 8到 .NET 7 为止堆的大小基本是跟着机器规格走的服务器 GC 在 48 核机器上起步就是几十个堆、每个堆都给一笔预算。但应用真正长期存活的数据可能只有几百 MB。.NET 82023引入DATASDynamic Adaptation To Application Sizes让堆的大小跟着数据走而不是跟着机器走。它先在 NativeAOT 项目里默认开启opt-in。DATAS 有两个机制① 堆的数量从 1 个起步按需长到核数个。进程启动时只用1 个堆省内存当分配线程开始排队等堆时逐个加堆保吞吐负载降下来再往回缩。判断依据是TCPThroughput Cost Percentage—— 用暂停 分配等待占总时间的比例来衡量代价默认目标 2%。② 两个预算一个管上限、一个管实际。上限由长期存活数据量决定防止堆无理由膨胀实际 gen0 预算由吞吐决定始终压在上限之下。负载变重时加堆、而不是无限加预算。上限的算法是一个开方式f(应用大小 MB) (20 − 省内存档位) ÷ √(应用大小 MB)结果先被钳位到[0.1, 10]再乘到应用大小上。实例应用大小 1 GB1000 MB省内存档位未设置时按 5 计算(20 − 5) ÷ √1000 15 ÷ 31.62 ≈ 0.474 → gen0 总预算 ≈ 1 GB × 47.4% ≈ 474 MB0.474 落在[0.1, 10]内钳位不生效直接采用。5.2 DATAS 默认开启.NET 9.NET 92024把 DATAS 在服务器 GC 上默认开启。官方基准TechEmpower JSON / Fortunes48 核 Linux给出的账是工作集下降 80% 以上≤ 20% 基线代价是最大吞吐下降 2–3%gen0 / gen1 回收次数变多但每次更小。怎么理解这个交换省下的是容器里真金白银的内存配额同机器能塞更多进程让出的是峰值吞吐的 2–3%而大多数业务跑不到峰值。如果你的瓶颈确实是极限吞吐直接关掉即可—— 项目文件里设GarbageCollectionAdaptationMode 0或用环境变量DOTNET_GCDynamicAdaptationMode0。.NET 9同时让GC.RefreshMemoryLimit()支持运行时修改内存上限并把 DATAS 的 gen0 预算做成可微调的三个旋钮DGen0GrowthPercent等。5.3 .NET 10一边让回收更准一边让 JIT 少分配.NET 102025沿两条线同时推进让回收更精确以及干脆不产生垃圾。① 写屏障write barrierArm64 也有动态切换了。写屏障是老对象指向新对象时通知 GC 的那条指令 —— 代价在写入收益在回收。x64 上早就支持按场景动态切换实现.NET 10 起 Arm64 同样支持且新的默认实现对 GC 区域的处理更精确。官方基准给出的收益是GC 暂停时间改善 8% 20% 以上代价是写屏障本身的吞吐略微下降 —— 典型的把成本从暂停挪到写入。区域的引入让空闲内存可以跨世代复用而写屏障决定 GC 能否精确知道谁引用了谁。两者一起才让 .NET 10 的回收更短、更少。② 逃逸分析escape analysis与栈分配。JIT 判断这个对象出不了当前方法就直接在栈上分配—— GC 根本不用管它。.NET 9 已支持装箱对象box的栈分配.NET 10大幅扩展小值类型数组不含 GC 指针、大小固定小引用类型数组生命周期不出方法结构体字段引用的对象结构体本身不逃逸不逃逸的委托对象闭包本身仍未栈分配官方性能博客给出的委托示例.NET 9 是 18,983 ns / 88 B.NET 10 是 6,292 ns / 24 B—— 约3× 快、分配字节少约 7 成而代码没改一行。方向已经很清楚GC 的性能故事正在从怎么更快地回收垃圾转向怎么根本不产生垃圾。栈分配减少的对象越多GC 要标记、要搬运、要维护写屏障的东西就越少 —— 这是复利式的收益。时代六 · 精细化与更大规模.NET 112026.NET 112026没有换架构。分代、区域、DATAS、后台回收全部照旧 —— 这一版做的是三件事把写屏障和回收元数据再精修一遍把省内存的旋钮再调细一档以及让运行时能在更大的机器上启动。.NET11的改进我们有专门文章往前翻就可以看到文章介绍了LXR GC的研发进度。.NET11 GC改进汇总汇总这24年的GC发展。看到一个反直觉的转折可调 GC 配置项的数量先一路暴涨Framework 4.5–4.8 约 9 项 → Core 1.0 12 项 → Core 3.0 20 项 → .NET 7 35 项在.NET 8 之后开始“收回”—— .NET 8 到 .NET 11 这四年一共只加了 9 项其中 .NET 11 全年只加了 1 项旋钮先变多再被运行时收回去。转折点在 .NET 8DATAS 把“堆该有多大”这个最难、最容易调错的决策收回到运行时。之后新增的旋钮目标 TCP、gen0 预算钳位、修剪保留年轻代不再是“你必须设”而是“自适应逻辑的微调”。粒度越来越细。段 1–4 GB → 区域 4 MB空闲内存第一次能被任何世代、任何堆抢去用decommit 也不再被“只能砍段尾”限制住。细粒度是 DATAS 能缩堆、能跨容器边界算账的前提。暂停越来越短。整次 gen2 停世界 → 后台标记 → 只剩 gen0/gen1 的前台短回收.NET 10 把 Arm64 的暂停再压 8–20%.NET 11 又把写屏障本身精修了一遍。今天一次 gen0 回收通常在微秒到毫秒级。少产生垃圾 快回收垃圾。从 POH 到栈分配是同一条思路把对象放到 GC 管不着的地方—— pin 的走 POH不逃逸的走栈剩下的才轮到 GC 操心。.NET 11 连“可空类型装箱的临时对象”也搬上了栈。回收本身越来越“精准”。.NET 11 的两处 GC 本体改动依赖句柄随龄、vxsort 扩到 Arm64都不是新特性而是让既有机制少做无用功—— 100 万句柄下 34.6× 的收益就是这么来的。版本速查表版本年份这一版做了什么Framework 1.02002分代 GC 定型gen0/1/2LOH≥ 85,000 字节工作站 / 服务器 GC 两种口味1.1 – 3.52003–2009并发 GCgen2 挪到专用线程分配受限2.0 补手动干预 APIFramework 4.02010后台 GCBGC取代并发 GC取消分配限制仅工作站Framework 4.52012BGC 扩展到服务器 GC默认LOH 空闲块复用 2 GB 数组解禁4.5.12013按需 LOH 压缩LargeObjectHeapCompactionMode4.62015NoGC RegionTryStartNoGCRegion/EndNoGCRegion4.6.22016服务器 GC 亲和性三件套HeapCount/NoAffinitize/HeapAffinitizeMask4.82019省内存模式GCConserveMemory 0…9Core 1.02016跨平台配置从app.config搬到runtimeconfig.jsonCore 3.02019容器一等公民GCHeapHardLimit/HeapHardLimitPercent按 cgroup 算亲和性全套GC.GetGCMemoryInfo()LOHThreshold.NET 52020POH 固定对象堆SOH / LOH / POH 分堆硬限制.NET 62021regions 在独立 GCclrgc.dll里孵化.NET 72022段 → 区域SOH 4 MB / UOH 32 MB默认预留 256 GB 地址空间 64 核默认跨 CPU 组.NET 82023DATAS 登场opt-inNativeAOT 默认开GC.RefreshMemoryLimit().NET 92024DATAS 在服务器 GC 上默认开启工作集 −80%吞吐 −2~3%.NET 102025Arm64 写屏障可动态切换暂停 −8~20%逃逸分析 / 栈分配大幅扩展DATAS gen0 预算增加三个旋钮.NET 112026写屏障精修展开数组协变检查 / 结构体复制走堆目标分析 SIMD / 相邻字段合并成更宽写入依赖句柄随龄100 万句柄 gen0 回收 34.6×vxsort 扩到 Arm64移除 1024 逻辑 CPU 上限GC 堆数上限仍 1024新增DOTNET_GCTrimYoungestKeepPercent压缩水位线修复大页吞吐 2.1% / 峰值 9.4%、OOM −35%可空装箱临时对象改走栈以上就是我们.NET GC整个24年一路走的路程可以看到以下特点内存管理越来越精细自动化程度越来越高最理想的情况应该是尽可能适应更多的情况。但这个内存和STW相斥的特点还是需要一些手动配置来微调行为的。我是搬砖工感谢大家的关注点赞您的鼓励是我持续更新的最大动力。我们下期见。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑