You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C#批量生成PDF附件时触发System.OutOfMemoryException异常

解决批量生成PDF附件时的System.OutOfMemoryException问题

从你的代码和描述来看,你在循环4000次生成个性化PDF附件时踩了内存溢出的坑——虽然系统还有7GB左右的空闲内存,但CLR的托管堆有自己的内存限制和碎片化问题,咱们来一步步拆解问题、解决它:

问题根源拆解

你遇到的OutOfMemoryException出现在MemoryStream.ToArray(),核心原因有两个:

  1. 内存堆积:循环中生成的所有PDF字节数组都存在Recipients列表里,直到整个方法结束才会被回收,4000个附件的内存持续累加,最终耗尽托管堆空间(哪怕系统物理内存还有剩余)。
  2. 内存复制与碎片: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 09:09:04