一:配景
1. 讲故事
前些天又碰到了一例 FileSystemWatcher 引发的内存碎片化故障,但这个碎片化不是由于经典的 reloadOnChange=true 导致的,以是我以为有须要做一次深度的反思,供以后碰到雷同标题提供技能上的办理方法,这篇我们就来体系的讲授下 两种碎片化方式的观察方法。
二:经典的 FileSystemWatcher 碎片化
1. 测试代码
这种碎片化是由 reloadOnChange=true 引发的,祸根紧张是步伐员将 .netframework 读取设置文件的方式套在了 .net 上,为了方便演示,先上一段测试代码。- internal class Program
- {
- static void Main(string[] args)
- {
- for (int i = 0; i < 100000; i++)
- {
- IConfiguration configuration = BuildConfiguration();
- string appName = configuration["AppName"];
- Console.WriteLine($"i={i} 应用名称: {appName}");
- }
- Console.ReadLine();
- }
- static IConfiguration BuildConfiguration()
- {
- return new ConfigurationBuilder()
- .SetBasePath(Directory.GetCurrentDirectory())
- .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
- .Build();
- }
- }
复制代码 卦中的代码非常简单,就是每次读取 AppName 时都调了一下 BuildConfiguration 方法,仅此而已,但将步伐跑起来之后,居然发现步伐吃了 2.2G 的内存,真是没边的事,截图如下:
为了找出缘故起因,上 windbg 附加,利用 !dumpheap -stat 观察托管堆,截图如下:
从卦中可以看到两点信息:
- Free 独占 1.39G,这是经典的内存碎片化。
- FileSystemWatcher 高达 1290 个,表明步伐存在大量的文件监控
。
看到上面两点信息,肯定要有条件反射,是不是 reloadOnChange: true 导致的。
2. 是 reloadOnChange 导致的吗
要想找到答案,可以深挖 Microsoft.Extensions.Configuration.ConfigurationRoot 类,即代码 BuildConfiguration(); 的返回范例,为了方便可视化观察,我用 vs 直接找下给各人看看,截图如下:

有了这个脉络,就可以利用 windbg 下钻观察,终极就找到了 k__BackingField = 1 的铁证,参考如下:- 0:008> !dumpobj /d 17dd2f41fa0
- Name: Microsoft.Extensions.Configuration.ConfigurationRoot
- MethodTable: 00007ff9d8707a48
- EEClass: 00007ff9d86e97b0
- Tracked Type: false
- Size: 40(0x28) bytes
- File: D:\travels\src\Example\Example_0_1\bin\Debug\net8.0\Microsoft.Extensions.Configuration.dll
- Fields:
- MT Field Offset Type VT Attr Value Name
- 00007ff9d8706c48 4000016 8 ...on.Abstractions]] 0 instance 0000017dd2f3e520 _providers
- 00007ff9d880ba28 4000017 10 ...Private.CoreLib]] 0 instance 0000017dd2f42018 _changeTokenRegistrations
- 00007ff9d8708940 4000018 18 ...rationReloadToken 0 instance 0000017dd2f41fc8 _changeToken
- 0:008> !DumpObj /d 0000017dd2f3e520
- Name: System.Collections.Generic.List`1[[Microsoft.Extensions.Configuration.IConfigurationProvider, Microsoft.Extensions.Configuration.Abstractions]]
- MethodTable: 00007ff9d87069d0
- EEClass: 00007ff9d86a10f8
- Tracked Type: false
- Size: 32(0x20) bytes
- File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.22\System.Private.CoreLib.dll
- Fields:
- MT Field Offset Type VT Attr Value Name
- 00007ff9d891d1a0 400226e 8 System.__Canon[] 0 instance 0000017dd2f41f68 _items
- 00007ff9d8551188 400226f 10 System.Int32 1 instance 1 _size
- 00007ff9d8551188 4002270 14 System.Int32 1 instance 1 _version
- 00007ff9d891d1a0 4002271 8 System.__Canon[] 0 static dynamic statics NYI s_emptyArray
- 0:008> !DumpArray /d 0000017dd2f41f68
- Name: Microsoft.Extensions.Configuration.IConfigurationProvider[]
- MethodTable: 00007ff9d8707cf0
- EEClass: 00007ff9d851c440
- Size: 56(0x38) bytes
- Array: Rank 1, Number of elements 4, Type CLASS
- Element Methodtable: 00007ff9d8706938
- [0] 0000017dd2f3e540
- [1] null
- [2] null
- [3] null
- 0:008> !DumpObj /d 0000017dd2f3e540
- Name: Microsoft.Extensions.Configuration.Json.JsonConfigurationProvider
- MethodTable: 00007ff9d8708200
- EEClass: 00007ff9d86e9ab8
- Tracked Type: false
- Size: 48(0x30) bytes
- File: D:\travels\src\Example\Example_0_1\bin\Debug\net8.0\Microsoft.Extensions.Configuration.Json.dll
- Fields:
- MT Field Offset Type VT Attr Value Name
- 00007ff9d8708940 4000012 8 ...rationReloadToken 0 instance 0000017dd2f437e0 _reloadToken
- 00007ff9d8708cf0 4000013 10 ...Private.CoreLib]] 0 instance 0000017dd2f42298 <Data>k__BackingField
- 00007ff9d8662820 4000005 18 System.IDisposable 0 instance 0000017dd2f3e690 _changeTokenRegistration
- 00007ff9d8701b98 4000006 20 ...nfigurationSource 0 instance 0000017dd2f3e4b8 <Source>k__BackingField
- 0:008> !DumpObj /d 0000017dd2f3e4b8
- Name: Microsoft.Extensions.Configuration.Json.JsonConfigurationSource
- MethodTable: 00007ff9d8701c88
- EEClass: 00007ff9d86e7868
- Tracked Type: false
- Size: 48(0x30) bytes
- File: D:\travels\src\Example\Example_0_1\bin\Debug\net8.0\Microsoft.Extensions.Configuration.Json.dll
- Fields:
- MT Field Offset Type VT Attr Value Name
- 00007ff9d86d8188 4000007 8 ...ers.IFileProvider 0 instance 0000017dd2f3e230 <FileProvider>k__BackingField
- 00007ff9d85cec08 4000008 10 System.String 0 instance 0000017d00100510 <Path>k__BackingField
- 00007ff9d851d070 4000009 24 System.Boolean 1 instance 0 <Optional>k__BackingField
- 00007ff9d851d070 400000a 25 System.Boolean 1 instance 1 <ReloadOnChange>k__BackingField
- 00007ff9d8551188 400000b 20 System.Int32 1 instance 250 <ReloadDelay>k__BackingField
- 00007ff9d8708420 400000c 18 ....FileExtensions]] 0 instance 0000000000000000 <OnLoadException>k__BackingField
复制代码 三:非经典的 FileSystemWatcher 碎片化
1. 测试代码
有的时间会出现 FileSystemWatcher 很少,但 overlapped 许多的环境,这种环境很大概率不是 reloadOnChange: true 导致的,截图如下:

像这种环境大概就须要开启追踪了,可以借助🐂👃的harmony 搞定,那怎样做呢?可以钩住 FileSystemWatcher 的全部构造函数,通过记录调用栈来观察到底是什么代码调用的,从而探求祸根,参考代码如下:- internal class Program
- {
- static void Main(string[] args)
- {
- var harmony = new Harmony("com.example.fswatcher");
- harmony.PatchAll();
- for (int i = 0; i < 5; i++)
- {
- IConfiguration configuration = BuildConfiguration();
- string appName = configuration["AppName"];
- Console.WriteLine($"i={i} 应用名称: {appName}");
- }
- Console.ReadLine();
- }
- static IConfiguration BuildConfiguration()
- {
- return new ConfigurationBuilder()
- .SetBasePath(Directory.GetCurrentDirectory())
- .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
- .Build();
- }
- }
- [HarmonyPatch]
- public class FileSystemWatcherConstructorsPatch
- {
- [HarmonyTargetMethod]
- static IEnumerable<MethodBase> TargetMethods()
- {
- // 一次性获取所有公共实例构造函数
- return typeof(FileSystemWatcher).GetConstructors(BindingFlags.Public | BindingFlags.Instance);
- }
- [HarmonyPostfix]
- public static void Postfix(FileSystemWatcher __instance)
- {
- Console.WriteLine($"[Harmony] FileSystemWatcher 构造函数被调用");
- Console.WriteLine($"[Harmony] 路径: '{__instance.Path ?? "null"}', 过滤器: '{__instance.Filter ?? "null"}'");
- Console.WriteLine($"[Harmony] 调用栈:");
- Console.WriteLine(Environment.StackTrace);
- }
- }
复制代码
从卦中可以看到,原来这个 FileSystemWatcher 是我们的用户代码 BuildConfiguration 搞的哈,这就极大的缩小的困绕圈,从而快速定位祸根。
四:总结
许多的内存碎片化每每都能看到 FileSystemWatcher 的身影,盼望这篇的反思和总结能给各人带来资助。 |