志高恒远 發表於 2025-11-21 11:21:00

对 .NET FileSystemWatcher引发内存碎片化的 反思

<h2 id="一背景">一:背景</h2>
<h3 id="1-讲故事">1. 讲故事</h3>
<p>前些天又遇到了一例 FileSystemWatcher 引发的内存碎片化故障,但这个碎片化不是因为经典的 <code>reloadOnChange=true</code> 导致的,所以我觉得有必要做一次深度的反思,供以后遇到类似问题提供技术上的解决方法,这篇我们就来系统的讲解下 两种碎片化方式的调查方法。</p>
<h2 id="二经典的-filesystemwatcher-碎片化">二:经典的 FileSystemWatcher 碎片化</h2>
<h3 id="1-测试代码">1. 测试代码</h3>
<p>这种碎片化是由 <code>reloadOnChange=true</code> 引发的,祸根主要是程序员将 .netframework 读取配置文件的方式套在了 .net 上,为了方便演示,先上一段测试代码。</p>
<pre><code class="language-C#">
    internal class Program
    {
      static void Main(string[] args)
      {
            for (int i = 0; i &lt; 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();
      }
    }

</code></pre>
<p>卦中的代码非常简单,就是每次读取 AppName 时都调了一下 BuildConfiguration 方法,仅此而已,但将程序跑起来之后,居然发现程序吃了 <code>2.2G</code> 的内存,真是没边的事,截图如下:</p>
<p><img src="https://img2024.cnblogs.com/blog/214741/202511/214741-20251121111955829-393023032.png" alt="" loading="lazy"></p>
<p>为了找出原因,上 windbg 附加,使用 <code>!dumpheap -stat</code> 观察托管堆,截图如下:</p>
<p><img src="https://img2024.cnblogs.com/blog/214741/202511/214741-20251121111955806-5408484.png" alt="" loading="lazy"></p>
<p>从卦中可以看到两点信息:</p>
<ol>
<li>Free 独占 <code>1.39G</code>,这是经典的内存碎片化。</li>
<li>FileSystemWatcher 高达 1290 个,表明程序存在大量的文件监控。</li>
</ol>
<p>看到上面两点信息,一定要有条件反射,是不是 <code>reloadOnChange: true</code> 导致的。</p>
<h3 id="2-是-reloadonchange-导致的吗">2. 是 reloadOnChange 导致的吗</h3>
<p>要想找到答案,可以深挖 <code>Microsoft.Extensions.Configuration.ConfigurationRoot</code> 类,即代码 <code>BuildConfiguration();</code> 的返回类型,为了方便可视化观察,我用 vs 直接找下给大家看看,截图如下:</p>
<p><img src="https://img2024.cnblogs.com/blog/214741/202511/214741-20251121111955812-1604614285.png" alt="" loading="lazy"></p>
<p>有了这个脉络,就可以使用 windbg 下钻观察,最终就找到了 <code>&lt;ReloadOnChange&gt;k__BackingField = 1</code> 的铁证,参考如下:</p>
<pre><code class="language-C#">
0:008&gt; !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
00007ff9d8706c484000016      8 ...on.Abstractions]]0 instance 0000017dd2f3e520 _providers
00007ff9d880ba284000017       10 ...Private.CoreLib]]0 instance 0000017dd2f42018 _changeTokenRegistrations
00007ff9d87089404000018       18 ...rationReloadToken0 instance 0000017dd2f41fc8 _changeToken
0:008&gt; !DumpObj /d 0000017dd2f3e520
Name:      System.Collections.Generic.List`1[]
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
00007ff9d891d1a0400226e      8   System.__Canon[]0 instance 0000017dd2f41f68 _items
00007ff9d8551188400226f       10         System.Int321 instance                1 _size
00007ff9d85511884002270       14         System.Int321 instance                1 _version
00007ff9d891d1a04002271      8   System.__Canon[]0   static dynamic statics NYI               s_emptyArray
0:008&gt; !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
0000017dd2f3e540
null
null
null
0:008&gt; !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
00007ff9d87089404000012      8 ...rationReloadToken0 instance 0000017dd2f437e0 _reloadToken
00007ff9d8708cf04000013       10 ...Private.CoreLib]]0 instance 0000017dd2f42298 &lt;Data&gt;k__BackingField
00007ff9d86628204000005       18   System.IDisposable0 instance 0000017dd2f3e690 _changeTokenRegistration
00007ff9d8701b984000006       20 ...nfigurationSource0 instance 0000017dd2f3e4b8 &lt;Source&gt;k__BackingField
0:008&gt; !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
00007ff9d86d81884000007      8 ...ers.IFileProvider0 instance 0000017dd2f3e230 &lt;FileProvider&gt;k__BackingField
00007ff9d85cec084000008       10      System.String0 instance 0000017d00100510 &lt;Path&gt;k__BackingField
00007ff9d851d0704000009       24       System.Boolean1 instance                0 &lt;Optional&gt;k__BackingField
00007ff9d851d070400000a       25       System.Boolean1 instance                1 &lt;ReloadOnChange&gt;k__BackingField
00007ff9d8551188400000b       20         System.Int321 instance            250 &lt;ReloadDelay&gt;k__BackingField
00007ff9d8708420400000c       18 ....FileExtensions]]0 instance 0000000000000000 &lt;OnLoadException&gt;k__BackingField

</code></pre>
<h2 id="三非经典的-filesystemwatcher-碎片化">三:非经典的 FileSystemWatcher 碎片化</h2>
<h3 id="1-测试代码-1">1. 测试代码</h3>
<p>有的时候会出现 <code>FileSystemWatcher</code> 很少,但 overlapped 很多的情况,这种情况很大概率不是 <code>reloadOnChange: true</code> 导致的,截图如下:</p>
<p><img src="https://img2024.cnblogs.com/blog/214741/202511/214741-20251121111955825-1579330148.png" alt="" loading="lazy"></p>
<p>像这种情况可能就需要开启追踪了,可以借助🐂👃的harmony 搞定,那如何做呢?可以钩住 <code>FileSystemWatcher</code> 的所有构造函数,通过记录调用栈来观察到底是什么代码调用的,从而寻找祸根,参考代码如下:</p>
<pre><code class="language-C#">
    internal class Program
    {
      static void Main(string[] args)
      {
            var harmony = new Harmony("com.example.fswatcher");
            harmony.PatchAll();

            for (int i = 0; i &lt; 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();
      }
    }

   
    public class FileSystemWatcherConstructorsPatch
    {
      
      static IEnumerable&lt;MethodBase&gt; TargetMethods()
      {
            // 一次性获取所有公共实例构造函数
            return typeof(FileSystemWatcher).GetConstructors(BindingFlags.Public | BindingFlags.Instance);
      }

      
      public static void Postfix(FileSystemWatcher __instance)
      {
            Console.WriteLine($" FileSystemWatcher 构造函数被调用");
            Console.WriteLine($" 路径: '{__instance.Path ?? "null"}', 过滤器: '{__instance.Filter ?? "null"}'");
            Console.WriteLine($" 调用栈:");
            Console.WriteLine(Environment.StackTrace);
      }
    }

</code></pre>
<p><img src="https://img2024.cnblogs.com/blog/214741/202511/214741-20251121111955815-142625454.png" alt="" loading="lazy"></p>
<p>从卦中可以看到,原来这个 <code>FileSystemWatcher</code> 是我们的用户代码 <code>BuildConfiguration</code> 搞的哈,这就极大的缩小的包围圈,从而快速定位祸根。</p>
<h2 id="四总结">四:总结</h2>
<p>很多的内存碎片化往往都能看到 FileSystemWatcher 的身影,希望这篇的反思和总结能给大家带来帮助。</p>
<img src="https://images.cnblogs.com/cnblogs_com/huangxincheng/345039/o_210929020104最新消息优惠促销公众号关注二维码.jpg" width="700" height="300" alt="图片名称" align="center"><br><br>
来源:https://www.cnblogs.com/huangxincheng/p/19251728
頁: [1]
查看完整版本: 对 .NET FileSystemWatcher引发内存碎片化的 反思