FileStream不关闭导致内存上涨的根本原因是未释放的SafeFileHandle阻止GC回收缓冲区和异步I/O对象;需用using包裹、避免FileShare.None长期持有、慎用ReadAllBytes、MemoryCache须设过期策略与大小限制、禁用同步等待及async void、外部进程需显式管理。

为什么 FileStream 不关会导致内存占用持续上涨
根本原因不是文件句柄本身吃内存,而是未释放的 FileStream 持有底层 SafeFileHandle,进而阻止 GC 回收关联的缓冲区(尤其是启用了 bufferSize 且值较大时)和异步 I/O 状态对象。常见于用 using 包裹失败、或在异常路径中漏掉 Dispose()。
实操建议:
- 所有手动创建的
FileStream必须用using块包裹,哪怕只是读几行——别信“小文件无所谓” - 避免在构造函数里传
FileShare.None后长期持有实例;若需复用,改用File.OpenRead()/File.OpenWrite()并确保上层控制生命周期 - 检查是否误用了
File.ReadAllBytes()或File.ReadAllText()处理大文件:它们会把整个内容加载进托管堆,且无流式节制
MemoryCache 缓存文件内容却没设过期策略
很多人用 MemoryCache 存 byte[] 或 string 表示文件内容,但忘了默认不淘汰——缓存键不变,数据就永远留着,GC 不动它,内存只增不减。
实操建议:
- 显式设置
absoluteExpiration或slidingExpiration,哪怕只是TimeSpan.FromMinutes(5) - 对大文件内容,优先考虑缓存
FileInfo或哈希值,而非原始字节;真要缓存内容,加size配置项限制总容量(MemoryCacheOptions.SizeLimit) - 用
GetOrCreateAsync时注意:如果工厂方法抛异常,缓存不会写入,但调用方可能重试多次,导致重复加载——应在工厂内部做 try/catch + fallback
异步文件操作中 async/await 链断裂引发资源滞留
典型现象是 CPU 不高、线程数正常,但私有字节数(Private Bytes)缓慢爬升,尤其在 ASP.NET Core 中高频上传/下载场景。根源常是 await 后续丢失上下文,导致 FileStream 的终结器线程无法及时触发 Dispose,缓冲区卡在 finalizer queue。
实操建议:
- 绝不写
stream.ReadAsync(...).GetAwaiter().GetResult()这类同步等待——它会阻塞线程并可能破坏异步流的资源清理链 - 在
async void方法(如事件处理器)中做文件操作?立刻改掉。必须用async Task,让调用方能 await 和捕获异常 - 使用
FileStream构造时显式传useAsync: true(.NET 5+ 默认开启),并确认你没有在ConfigureAwait(false)后又意外切回 UI/ASP.NET 上下文导致死锁式等待
用 Process.Start("notepad.exe", path) 打开文件却不跟踪子进程
这不是 .NET 文件 API 的问题,但极易被归为“文件操作泄漏”:启动外部进程打开文件后,若不保存 Process 实例、也不处理其 Exited 事件或调用 WaitForExit(),该进程的句柄会持续占用非托管内存,且 Windows 不会自动回收其映射的文件视图(MapViewOfFile)。
实操建议:
- 启动进程后立即保存
Process引用,必要时在父逻辑结束前调用process.Kill()(注意权限) - 避免用
Process.Start()直接打开用户文档——改用ShellExecute(P/InvokeShellExecuteEx并设SEE_MASK_NOCLOSEPROCESS)可获得更可控的句柄 - 若只是想预览,优先走
Windows.System.Launcher.LaunchFileAsync()(UWP/WinUI),它由系统托管生命周期
最麻烦的从来不是哪行代码漏了 Dispose,而是多个小疏忽叠在一起:缓存没限大小 + 流没用 using + 异步链断了 + 外部进程失控——它们各自只涨一点内存,合起来就让诊断工具看不出主因。










