惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

J
Java Code Geeks
Stack Overflow Blog
Stack Overflow Blog
P
Proofpoint News Feed
L
LangChain Blog
C
Check Point Blog
F
Fortinet All Blogs
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Blog — PlanetScale
Blog — PlanetScale
腾讯CDC
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Jina AI
Jina AI
Recent Announcements
Recent Announcements
雷峰网
雷峰网
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
N
Netflix TechBlog - Medium
博客园 - 【当耐特】
Hugging Face - Blog
Hugging Face - Blog
Microsoft Azure Blog
Microsoft Azure Blog
U
Unit 42
The Cloudflare Blog
月光博客
月光博客
有赞技术团队
有赞技术团队
G
Google Developers Blog
Vercel News
Vercel News

博客园 - 一线码农

记一次 .NET 某珠宝公司内部管理系统 内存暴涨分析 记一次 .NET 某智慧医保云服务Linux 非托管泄露分析 .NET 高级调试技术:超越基础 Dump 分析 .NET 生产环境调试实战指南 记一次 .NET 某光电成像后端解析系统 内存暴涨分析 记一次 .NET 某电力后台监控系统 内存暴涨分析 记一次 .NET 某注塑模具系统 CPU爆高分析 记一次 .NET 某集群管理软件 内存暴涨分析 记一次 .NET 某智慧工厂视觉程序 崩溃分析 记一次 .NET 某低代码开发框架 内存暴涨分析 记一次 .NET 某MES上位机拍照系统 内存暴涨分析 记一次 .NET 某RFID标签打印客户端 崩溃分析 DotMemory系列:5. 如何实现自动化抓取和应用自托管 DotMemory系列:4. 如何分析进程的转储文件 DotMemory系列:3. 堆碎片化引发的内存暴涨分析 DotMemory系列:2. 事件泄露引发的内存暴涨分析 DotMemory系列:1. 终结队列积压引发的内存暴涨分析 记一次 .NET 某理财管理客户端 OOM溢出分析 记一次 .NET 某医联体管理系统 崩溃分析 记一次 .NET 某光放测试系统 崩溃分析 记一次 .NET 某药品缺陷高速检测系统 卡慢分析 聊一聊 .NET超高内存故障分析方法 的反思 记一次 .NET 某企业ECM内容管理系统 内存暴涨分析 记一次 .NET 某跨境物流系统 内存暴涨分析 记一次 .NET 某中医药附属医院门诊系统 崩溃分析 聊一聊 .NET 中的 CompositeChangeToken 聊一聊 .NET 中的 CancellationTokenSource 记一次 .NET 某CRM物流行业管理系统 崩溃分析
对 .NET FileSystemWatcher引发内存碎片化的 反思
一线码农 · 2025-11-21 · via 博客园 - 一线码农

一:背景

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 观察托管堆,截图如下:

从卦中可以看到两点信息:

  1. Free 独占 1.39G,这是经典的内存碎片化。
  2. 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:\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 的身影,希望这篇的反思和总结能给大家带来帮助。

图片名称