C#使用iText7合并PDF后内存未释放问题及Close()作用咨询
C# iText7 批量PDF合并内存泄漏修复
问题现象
在C#环境下使用iText7开发文件夹批量PDF合并功能,交互逻辑为点击按钮弹出FolderBrowserDialog对话框,用户选中目标文件夹后,程序遍历文件夹内所有PDF文件执行合并。功能可正常运行,但合并完成后已处理的PDF资源始终驻留内存未释放,进程内存占用随合并操作次数持续上涨,各阶段内存占用如下:
- C#进程初始内存占用:

- 合并第一个文件夹后内存占用:

- 合并第二个文件夹后内存占用:

- 任务管理器显示的进程内存占用:

此前误以为调用Close()方法即可将对应资源从内存中清除,但实际未达到预期效果,需要明确Close()方法的实际作用,同时修复内存持续上涨的问题。
原有问题代码
private void button1_Click(object sender, EventArgs e) { folderBrowserDialog1 = new FolderBrowserDialog(); DialogResult = folderBrowserDialog1.ShowDialog(); if(DialogResult == DialogResult.OK) { textBox1.Text = folderBrowserDialog1.SelectedPath; Environment.SpecialFolder root = folderBrowserDialog1.RootFolder; } if(folderBrowserDialog1.SelectedPath != "") { IList<string> inputFiles = Directory.GetFiles(folderBrowserDialog1.SelectedPath); //合并文件输出路径 string dest = @"D:\PDF-merge"; string filename = "merged.pdf"; try { System.IO.Directory.CreateDirectory(dest); } catch { } //初始化合并目标文档 PdfDocument pdfDoc = new PdfDocument(new PdfWriter(dest + @"\" + filename)); PdfMerger merger = new PdfMerger(pdfDoc); PdfReader reader; PdfDocument pdf2merge; for (int i = 0; i < inputFiles.Count; i++) { if (Path.GetExtension(inputFiles[i]) == ".pdf") { reader = new PdfReader(inputFiles[i]); reader.SetUnethicalReading(true); pdf2merge = new PdfDocument(reader); merger.Merge(pdf2merge, 1, pdf2merge.GetNumberOfPages()); reader.Close(); pdf2merge.Close(); } } merger.Close(); } }
核心原因说明
Close()方法的实际作用
iText7中Close()方法的核心逻辑是:释放关联的非托管资源(文件句柄、流锁)、将内存缓存中未持久化的数据刷入底层流、关闭关联的IO流。它不会立刻强制.NET垃圾回收器回收托管堆上的对象内存,托管内存的回收由GC按自身调度策略触发,这也是调用Close后内存不会立刻下降的原因之一,但原代码存在更直接的资源泄漏点:
- 输出端的
PdfDocument pdfDoc全程未调用Close()/Dispose():关闭PdfMerger不会自动释放其绑定的输出PdfDocument实例,最终输出文档的资源会持续被占用。 - 手动调用Close的逻辑存在风险:如果合并过程中抛出异常(比如损坏的PDF文件、文件被占用),Close方法不会被执行,对应资源会直接泄漏。
- 未开启
PdfMerger的自动关闭源文档配置,源文档的部分页面缓存资源不会在单个文件合并完成后立刻释放。
修复方案
所有iText7的文档、读写器、合并器对象都实现了IDisposable接口,使用using语句包裹是最稳妥的资源释放方式——using会在代码块执行结束时,无论是否抛出异常都自动调用对象的Dispose()方法(iText7中Dispose和Close的执行逻辑完全一致),从根源上避免资源泄漏。
修复后的完整代码如下:
private void button1_Click(object sender, EventArgs e) { folderBrowserDialog1 = new FolderBrowserDialog(); DialogResult dr = folderBrowserDialog1.ShowDialog(); // 用户取消选择或路径为空时直接返回,避免后续逻辑异常 if(dr != DialogResult.OK || string.IsNullOrWhiteSpace(folderBrowserDialog1.SelectedPath)) { return; } textBox1.Text = folderBrowserDialog1.SelectedPath; // 原代码中root变量赋值后未使用,已移除 // 直接筛选pdf后缀文件,无需循环内判断扩展名 string[] inputFiles = Directory.GetFiles(folderBrowserDialog1.SelectedPath, "*.pdf"); string dest = @"D:\PDF-merge"; string outputPath = Path.Combine(dest, "merged.pdf"); Directory.CreateDirectory(dest); // 所有IDisposable对象用using块包裹,自动释放资源 using (PdfDocument targetPdfDoc = new PdfDocument(new PdfWriter(outputPath))) using (PdfMerger merger = new PdfMerger(targetPdfDoc)) { // 开启配置:单个文件合并完成后自动关闭源文档,无需手动调用Close merger.SetCloseSourceDocuments(true); foreach (string sourcePdfPath in inputFiles) { using (PdfReader reader = new PdfReader(sourcePdfPath)) { reader.SetUnethicalReading(true); using (PdfDocument sourcePdfDoc = new PdfDocument(reader)) { merger.Merge(sourcePdfDoc, 1, sourcePdfDoc.GetNumberOfPages()); } } } } // 代码执行到此处时,所有iText相关资源会被自动释放,文件句柄全部关闭 // 仅用于验证内存释放效果时调用,生产环境不建议频繁手动触发GC GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); }
修复点说明
- 新增对话框选择结果判断,避免用户取消选择后进入异常逻辑
- 使用
Path.Combine拼接路径,避免硬编码转义字符问题 - 所有iText7可释放对象全部用
using块包裹,异常场景下也能保证资源释放 - 开启
SetCloseSourceDocuments(true)配置,单个源文件合并完成后立刻释放对应资源 - 直接通过
Directory.GetFiles的筛选参数获取PDF文件,减少循环内冗余判断 - 修复原代码中未关闭输出端
PdfDocument的核心泄漏点
内容的提问来源于stack exchange,提问作者Joshua B
相关产品推荐
相关产品推荐

