C#批量生成PDF附件时触发System.OutOfMemoryException异常
解决批量生成PDF附件时的System.OutOfMemoryException问题
从你的代码和描述来看,你在循环4000次生成个性化PDF附件时踩了内存溢出的坑——虽然系统还有7GB左右的空闲内存,但CLR的托管堆有自己的内存限制和碎片化问题,咱们来一步步拆解问题、解决它:
问题根源拆解
你遇到的OutOfMemoryException出现在MemoryStream.ToArray(),核心原因有两个:
- 内存堆积:循环中生成的所有PDF字节数组都存在
Recipients列表里,直到整个方法结束才会被回收,4000个附件的内存持续累加,最终耗尽托管堆空间(哪怕系统物理内存还有剩余)。 - 内存复制与碎片:
MemoryStream.ToArray()会创建一个字节数组的副本,加上iTextSharp处理PDF时的临时内存占用,容易在大型对象堆(LOH)产生碎片,导致明明有内存却无法分配连续空间。
具体优化方案
1. 优化PDF生成方法的内存使用
调整GeneratePdfFromPdfFile的资源释放顺序,减少不必要的内存复制,同时给MemoryStream预设初始容量(基于模板大小),降低扩容带来的碎片:
private static byte[] GeneratePdfFromPdfFile(byte[] file, string landingPage, string code) { try { // 直接用传入的模板字节数组创建PdfReader,避免额外内存拷贝 using (var reader = new PdfReader(file)) { // 预设MemoryStream初始容量为模板大小,减少扩容次数 using (var ms = new MemoryStream(file.Length)) { using (var stamper = new PdfStamper(reader, ms)) { string _embeddedURL = $"http://{landingPage}/Default.aspx?code={code}&m={eventCode18}"; PdfAction act = new PdfAction(_embeddedURL); stamper.Writer.SetOpenAction(act); // using会自动释放stamper,无需手动调用Close() } // 此时reader和stamper已释放,内存压力更小 return ms.ToArray(); } } } catch(Exception ex) { string logPath = Path.Combine(HttpRuntime.AppDomainAppPath, "AttachmentException.txt"); File.WriteAllText(logPath, $"{ex.Message}\n{ex.StackTrace}"); return null; } }
2. 避免一次性持有所有附件内存
如果你的流程是生成附件后直接发送邮件,建议生成一个处理一个,处理完成后立即将item.EmailAttachment设为null,让GC能及时回收内存:
if (IncludeAttachment) { item.AttachmentName = _cmpTmp.getAttachmentSubjectbyLangCode( string.IsNullOrEmpty(item.Language) ? item.DefaultLanguage : item.Language, item.DefaultLanguage); item.EmailAttachment = AttachmentGeneratorEngine.GenerateAttachment(...); // 立即处理邮件发送逻辑 SendEmailToRecipient(item); // 处理完成后清空引用,释放内存 item.EmailAttachment = null; }
3. 分批处理收件人列表
把4000个收件人拆成小批次(比如每500个一批),处理完一批后强制触发垃圾回收(仅在批量场景下谨慎使用):
int batchSize = 500; for (int i = 0; i < Recipients.Count; i += batchSize) { var currentBatch = Recipients.Skip(i).Take(batchSize).ToList(); foreach (var item in currentBatch) { // 原有的收件人处理逻辑... if (IncludeAttachment) { item.EmailAttachment = AttachmentGeneratorEngine.GenerateAttachment(...); // 处理后清空引用 item.EmailAttachment = null; } } // 触发GC回收当前批次的内存 GC.Collect(); GC.WaitForPendingFinalizers(); }
4. 启用大型对象堆压缩(.NET Framework 4.5+)
大量PDF字节数组会进入LOH,LOH默认不会自动压缩,容易产生碎片。在web.config或app.config中添加配置:
<runtime> <gcAllowVeryLargeObjects enabled="true" /> <gcServer enabled="true" /> <HeapCompactionMode enabled="true" /> </runtime>
5. 切换为64位进程
如果你的应用是32位的,哪怕系统有16GB内存,CLR托管堆上限也只有约2GB。右键项目属性→生成→目标平台,改为x64,解锁更多内存空间。
6. 缓存PDF模板
如果_cmpTmp.getFirstAttachment(...)每次都从文件加载模板,建议缓存模板的字节数组,避免重复IO和内存加载:
// 用静态字典缓存模板,key为语言组合 private static readonly Dictionary<string, byte[]> _templateCache = new Dictionary<string, byte[]>(); public byte[] getFirstAttachment(string language, string defaultLanguage) { string cacheKey = $"{language}_{defaultLanguage}"; if (_templateCache.TryGetValue(cacheKey, out byte[] template)) { return template; } // 原有的模板加载逻辑 template = LoadTemplateFromFile(...); _templateCache[cacheKey] = template; return template; }
额外建议
用Visual Studio的诊断工具或PerfView监控托管堆的内存使用,确认是哪些对象占用了大量内存,针对性优化。
内容的提问来源于stack exchange,提问作者B. Abdo
相关产品推荐
相关产品推荐

