一:背景
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 下钻观察,最终就找到了 <ReloadOnChange>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:travelssrcExampleExample_0_1binDebugnet8.0Microsoft.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 FilesdotnetsharedMicrosoft.NETCore.App8.0.22System.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:travelssrcExampleExample_0_1binDebugnet8.0Microsoft.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:travelssrcExampleExample_0_1binDebugnet8.0Microsoft.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 的身影,希望这篇的反思和总结能给大家带来帮助。