二:内存暴涨分析
1. 为什么会内存暴涨
分析从常规命令开始,先用 !address -summary 观察内存分布情况,输出如下:
0:000> !address -summary
--- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
Free 3637dfd`e87c7000 ( 125.992 TB) 98.43%
<unknown> 9276201`e5858000 ( 2.007 TB) 99.96% 1.57%
Heap 650`2547f000 ( 596.496 MB) 0.03% 0.00%
Image 18550`09d35000 ( 157.207 MB) 0.01% 0.00%
Stack 930`02c00000 ( 44.000 MB) 0.00% 0.00%
Other 90`001de000 ( 1.867 MB) 0.00% 0.00%
TEB 310`0003e000 ( 248.000 kB) 0.00% 0.00%
PEB 10`00001000 ( 4.000 kB) 0.00% 0.00%
--- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_FREE 3637dfd`e87c7000 ( 125.992 TB) 98.43%
MEM_RESERVE 690201`2b6d4000 ( 2.005 TB) 99.82% 1.57%
MEM_COMMIT 106400`ec155000 ( 3.689 GB) 0.18% 0.00%
可以看到总计约 3.6G 的已提交内存,主要落在了 <unknown> 区域,我们推测是托管堆吃掉了。接下来使用 !dumpheap -stat 观察托管堆统计,输出如下:
0:000> !dumpheap -stat
Statistics:
MT Count TotalSize Class Name
...
0179c7715cb0 1,847,901,451,265,880 Free
7ffc6e0a2888 2,536,870,960 System.WeakReference<Microsoft.Extensions.DependencyInjection.ServiceProvider>[]
7ffc6e0a2260 60,873,9781,460,975,472 System.WeakReference<Microsoft.Extensions.DependencyInjection.ServiceProvider>
Total 63,333,893 objects, 2,494,520,292 bytes
结果令人吃惊,程序中有超过 6000 万个 System.WeakReference<Microsoft.Extensions.DependencyInjection.ServiceProvider> 对象!这正是内存暴涨的元凶。
接下来使用 !dumpheap -mt 7ffc6e0a2260 查看这些弱引用的详情,并尝试用 !gcroot 追踪它们的引用根,但过程并不顺利。
0:000> !dumpheap -mt 7ffc6e0a2260
Address MT Size
017988001000 7ffc6e0a2260 24
017988001018 7ffc6e0a2260 24
017988001030 7ffc6e0a2260 24
017988001048 7ffc6e0a2260 24
017988001060 7ffc6e0a2260 24
017988001078 7ffc6e0a2260 24
017988001090 7ffc6e0a2260 24
0179880010a8 7ffc6e0a2260 24
...
017a405f1020 7ffc6e0a2260 24
0:000> !gcroot 0179880010a8
Caching GC roots, this may take a while.
Subsequent runs of this command will be faster.
等待了超过20分钟也没有结果,可能是 6000 多万个对象的引用关系过于复杂,导致 WinDbg 不堪重负。既然此路不通,我们换个思路,采用内存搜索法来定位这些弱引用对象的“上级”。
选择列表末尾的 017a405f1020 这个对象作为突破口。
0:000> !dumpobj /d 17a405f1020
Name: System.WeakReference`1[[Microsoft.Extensions.DependencyInjection.ServiceProvider, Microsoft.Extensions.DependencyInjection]][]
MethodTable: 00007ffc6e0a2888
EEClass: 00007ffc6dbeb4f8
Tracked Type: false
Size: 536870936(0x20000018) bytes
Array: Rank 1, Number of elements 67108864, Type CLASS (Print Array)
Fields:
None
0:000> s-q 0 L?0xffffffffffffffff 17a405f1020
00000179`c95861d0 0000017a`405f1020 03a0dcfa`03a0dcfa
0:000> !lno 0000017a`405f1020
Before: 017a405f1000 32 (0x20) Free
Current: 017a405f1020 24 (0x18) System.WeakReference<Microsoft.Extensions.DependencyInjection.ServiceProvider>[]
Error Detected: Object 17a405f1020 has a bad member at offset 12054c00: ??? [verify heap]
Could not find object after 17a405f1020
Heap local consistency not confirmed.
0:000> !lno 00000179`c95861d0
Before: 0179c95861c8 32 (0x20) System.Collections.Generic.List<System.WeakReference<Microsoft.Extensions.DependencyInjection.ServiceProvider>>
Next: 0179c95861e8 24 (0x18) System.WeakReference<Microsoft.Extensions.DependencyInjection.ServiceProvider>[]
Heap local consistency confirmed.
0:000> !dumpobj /d 179c95861c8
Name: System.Collections.Generic.List`1[[System.WeakReference`1[[Microsoft.Extensions.DependencyInjection.ServiceProvider, Microsoft.Extensions.DependencyInjection]], System.Private.CoreLib]]
MethodTable: 00007ffc6e0a2340
EEClass: 00007ffc6dce0000
Tracked Type: false
Size: 32(0x20) bytes
File: D:\xxx\A_api\System.Private.CoreLib.dll
Fields:
MT Field Offset Type VT Attr Value Name
00007ffc6de328f0 400209f 8 System.__Canon[] 0 instance 0000017a405f1020 _items
00007ffc6dc894b0 40020a0 10 System.Int32 1 instance 60873978 _size
00007ffc6dc894b0 40020a1 14 System.Int32 1 instance 60873978 _version
00007ffc6de328f0 40020a2 8 System.__Canon[] 0 static dynamic statics NYI s_emptyArray
0:000> s-q 0 L?0xffffffffffffffff 179c95861c8
00000179`c77571d8 00000179`c95861c8 00000000`00000000
00000179`c95861b8 00000179`c95861c8 0800004e`00000000
0:000> !lno 00000179`c77571d8
Failed to find the segment of the managed heap where the object 179c77571d8 resides
0:000> !lno 00000179`c95861b8
Before: 0179c9586108 192 (0xc0) Microsoft.Extensions.DependencyInjection.DependencyInjectionEventSource
Next: 0179c95861c8 32 (0x20) System.Collections.Generic.List<System.WeakReference<Microsoft.Extensions.DependencyInjection.ServiceProvider>>
Heap local consistency confirmed.

根据搜索和对象分析,线索最终指向了 DependencyInjectionEventSource 类中的 _providers 字段,就是这个 List<WeakReference<ServiceProvider>> 承载了海量的弱引用对象,导致了内存暴涨。
2. DependencyInjectionEventSource 是什么
从名字上看,它与 ETW 事件追踪有关。我们先通过 !eeversion 确认 .NET 运行时版本,以便查找对应的源代码。
0:000> !eeversion
6.0.3624.51421 free
6,0,3624,51421 @Commit: f1dd57165bfd91875761329ac3a8b17f6606ad18
Workstation mode
SOS Version: 9.0.13.2701 retail build
环境是 .NET 6。为了理解其行为,我们需要查看源码。很快,我们在 .NET 运行时仓库的 DependencyInjectionEventSource.cs 文件中找到了相关代码。

从源码中可以看到,每当调用 BuildServiceProvider 构建一个服务提供者时,框架会将其包装成一个 WeakReference<ServiceProvider> 并添加到一个全局的 _providers 列表中。这个设计初衷可能是为了进行一些诊断或统计。
但问题就出在清理机制上。本着“我不是第一个遇到此问题的人”的想法,我尝试搜索了一下,果然发现 .NET 团队的 Stephen Toub 大佬在 2025 年 4 月就报告了这个问题,并在 .NET 10 中进行了修复。这被定义为一个优化级的 Bug。相关 issue 链接: https://github.com/dotnet/runtime/issues/114599 。


修复的核心逻辑在于:在向 _providers 列表添加新弱引用之前,会先检查列表状态。如果列表中存在大量已失效(被GC回收)的弱引用,就会触发一次清理操作,移除这些“僵尸”引用,防止列表无限膨胀。
修复后的 ServiceProviderBuilt 方法代码如下:
[NonEvent]
public void ServiceProviderBuilt(ServiceProvider provider)
{
lock (_providers)
{
int providersCount = _providers.Count;
if (providersCount > 0 &&
(_survivingProvidersCount is int spc ? (uint)providersCount >= 2 * (uint)spc : providersCount == _providers.Capacity))
{
_providers.RemoveAll(static p => !p.TryGetTarget(out _));
_survivingProvidersCount = _providers.Count;
}
_providers.Add(new WeakReference<ServiceProvider>(provider));
}
WriteServiceProviderBuilt(provider);
}
那么,在 .NET 6/7/8 中,什么情况下会触发这个问题呢?根据官方的描述和源码分析,关键在于创建的 ServiceProvider 没有及时调用 Dispose 方法。当 ServiceProvider 被释放时,它会调用 ServiceProviderDisposed 方法,该方法会遍历 _providers 列表,移除自身对应的弱引用,并顺便清理其他已失效的引用。
如果大量 ServiceProvider 只创建不释放,那么虽然 ServiceProvider 对象本身会被 GC 回收(因为是弱引用),但包裹它们的 WeakReference 对象却会一直留在列表中,导致列表长度和内存占用无限增长。
因此,这个问题是框架设计(在特定版本中)与应用程序使用习惯共同作用的结果。
解决办法有两种:
- 检查并修复应用程序代码:仔细排查代码中所有调用
BuildServiceProvider 的地方(尤其是在循环或高频路径中),确保创建的 ServiceProvider 在使用完毕后被正确 Dispose,或者使用 using 语句块。
- 升级运行时版本:将程序升级到 .NET 10 或更高版本,这是最直接、最根本的解决方案。
将分析结论和解决方案反馈给朋友后,他经过一番排查,最终锁定了问题根源:在某个高频调用的方法中,一个第三方类库内部不慎调用了 BuildServiceProvider 但没有管理其生命周期。修复后,内存恢复正常。

这次 内存泄漏 排查之旅再次证明,在复杂的 .NET 应用环境中,熟练使用 WinDbg 等调试工具进行 内存管理 分析是多么重要。清晰的排查思路和对框架底层机制的了解,能帮助我们在海量数据中迅速定位问题关键。如果你也在进行类似的性能调优或故障排查,欢迎来 云栈社区 交流分享你的经验与心得。