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

使用iText7多线程批量生成PDF时遭遇OutOfMemoryError求助

优化点及解决方案

1. 复用预解析的图片资源,避免重复处理Base64

每次生成PDF都解析Base64字符串为图片,会重复消耗CPU和内存。应提前将Base64转成Image对象缓存,复用给所有任务:

修改步骤:

  • 提前解析信头和签名的Base64为Image,传入任务而非Base64字符串
  • 重写BackgroundImageHandler接收Image而非Base64

示例代码:

// 预解析图片(若配置通用可全局缓存;若每个notice配置不同,可在createPdfConfiguration时解析)
public Image getLetterheadImage(PdfConfiguration configuration) {
    String base64 = configuration.getLetterheadBase64();
    byte[] imageBytes = Base64.getDecoder().decode(base64);
    return ImageDataFactory.create(imageBytes).createImage();
}

// 修改BackgroundImageHandler构造方法
class BackgroundImageHandler implements IEventHandler {
    private final Image letterhead;

    public BackgroundImageHandler(Image letterhead) {
        this.letterhead = letterhead;
    }

    @Override
    public void handleEvent(Event event) {
        PdfDocumentEvent docEvent = (PdfDocumentEvent) event;
        PdfDocument pdfDoc = docEvent.getDocument();
        PdfPage page = docEvent.getPage();
        Rectangle pageSize = page.getPageSize();
        // 直接复用预解析的Image
        letterhead.scaleToFit(pageSize.getWidth(), pageSize.getHeight());
        letterhead.setFixedPosition(page, 0, 0);
        new PdfCanvas(page.newContentStreamBefore(), page.getResources(), pdfDoc).addImage(letterhead);
    }
}

2. 优化资源关闭逻辑,避免重复关闭与内存泄漏

Document.close()会自动关闭关联的PdfDocument和PdfWriter,重复调用pdfDocument.close()可能导致资源释放异常。同时需确保所有资源在try-finally块中正确关闭,避免异常时泄漏:

修改startPdfCreation方法:

public byte[] startPdfCreation(String template, PdfConfiguration configuration, Map<String, String> notice, Image letterhead, ConverterProperties sharedProperties) {
    ByteArrayOutputStream outputStream = null;
    PdfWriter writer = null;
    PdfDocument pdfDocument = null;
    Document document = null;
    try {
        outputStream = new ByteArrayOutputStream(1024 * 1024); // 预估PDF大小设置初始容量,避免频繁扩容
        writer = new PdfWriter(outputStream);
        pdfDocument = new PdfDocument(writer);
        pdfDocument.setDefaultPageSize(PageSize.A4);
        document = new Document(pdfDocument);
        document.setLeftMargin(26);
        document.setRightMargin(26);

        pdfDocument.addEventHandler(PdfDocumentEvent.END_PAGE, new BackgroundImageHandler(letterhead));
        String substitutedTemplate = substituteValues(template, notice);

        List<IElement> elements = HtmlConverter.convertToElements(substitutedTemplate, sharedProperties);
        for (IElement element : elements) {
            if (element instanceof BlockElement) {
                document.add((BlockElement) element);
            }
        }
        document.close(); // 自动关闭pdfDocument和writer
        return outputStream.toByteArray();
    } catch (Exception e) {
        log.error("error while creating a pdf", e);
        return new byte[0];
    } finally {
        // 兜底关闭,防止异常时资源未释放
        try {
            if (document != null && !document.isClosed()) document.close();
        } catch (Exception ignored) {}
        try {
            if (outputStream != null) outputStream.close();
        } catch (Exception ignored) {}
    }
}

3. 避免内存中存储完整PDF字节数组

将PDF直接写入临时文件或上传流,而非先转成byte[]保存在内存中。若必须用内存,可设置ByteArrayOutputStream初始容量减少扩容开销:

示例:直接写入临时文件后上传

// 修改startPdfCreation返回文件路径而非byte[]
public String startPdfCreation(String template, PdfConfiguration configuration, Map<String, String> notice, Image letterhead, ConverterProperties sharedProperties) {
    File tempFile = File.createTempFile("pdf-", ".tmp");
    try (PdfWriter writer = new PdfWriter(tempFile);
         PdfDocument pdfDocument = new PdfDocument(writer);
         Document document = new Document(pdfDocument)) {
        pdfDocument.setDefaultPageSize(PageSize.A4);
        document.setLeftMargin(26);
        document.setRightMargin(26);

        pdfDocument.addEventHandler(PdfDocumentEvent.END_PAGE, new BackgroundImageHandler(letterhead));
        String substitutedTemplate = substituteValues(template, notice);

        List<IElement> elements = HtmlConverter.convertToElements(substitutedTemplate, sharedProperties);
        for (IElement element : elements) {
            if (element instanceof BlockElement) {
                document.add((BlockElement) element);
            }
        }
        return tempFile.getAbsolutePath();
    } catch (Exception e) {
        log.error("error while creating a pdf", e);
        tempFile.delete();
        return null;
    }
}

// 上传时读取临时文件
Map<String, String> uploadResponse = uploadPdf(new FileInputStream(tempFile), notice);
// 上传完成后删除临时文件
new File(tempFilePath).delete();

4. 调整线程池参数,避免内存过载

固定10线程可能导致内存占用叠加,建议根据CPU核心数设置线程数,同时限制任务队列大小,避免一次性提交1000个任务导致队列内存溢出:

// 自定义线程池,控制并发数与队列容量
int corePoolSize = Runtime.getRuntime().availableProcessors();
int maxPoolSize = corePoolSize + 2;
int queueCapacity = 50; // 限制队列等待任务数
ExecutorService executor = new ThreadPoolExecutor(
    corePoolSize,
    maxPoolSize,
    60L,
    TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(queueCapacity),
    new ThreadPoolExecutor.CallerRunsPolicy() // 队列满时让提交线程帮忙执行,避免任务被拒绝
);

5. 使用线程安全集合,避免并发问题与内存泄漏

普通ArrayList在多线程下会出现并发修改异常,且扩容会额外占用内存。改用线程安全的ConcurrentLinkedQueue:

Queue<Notice> successFiles = new ConcurrentLinkedQueue<>();
Queue<Notice> failedFiles = new ConcurrentLinkedQueue<>();

// 任务中添加元素
if (uploadResponse != null) {
    successFiles.add(notice);
} else {
    failedFiles.add(notice);
}

6. 复用ConverterProperties,避免重复初始化

ConverterProperties包含字体、样式等配置,每次创建会重复加载资源。全局创建一次复用给所有任务:

// 全局初始化一次
ConverterProperties sharedProperties = getConverterProperties();

// 任务中直接传入复用的对象
byte[] fileContent = startPdfCreation(template, pdfConfiguration, notice, letterhead, sharedProperties);

7. 优化模板替换逻辑,减少字符串内存占用

若模板较大,每次substituteValues生成新字符串会占用大量内存。可使用模板引擎(如FreeMarker)的缓存机制,复用编译后的模板对象,避免重复解析与字符串拼接:

// 预编译模板(全局缓存)
Configuration freemarkerConfig = new Configuration(Configuration.VERSION_2_3_32);
Template template = freemarkerConfig.getTemplate("template.ftl");

// 任务中直接渲染模板
StringWriter writer = new StringWriter();
template.process(notice, writer);
String substitutedTemplate = writer.toString();
总结优化后的流程
  1. 预解析所有共用图片资源为Image对象并缓存
  2. 全局初始化并复用ConverterProperties与编译后的模板
  3. 使用自定义线程池控制并发数与队列大小
  4. 生成PDF时优先写入临时文件,避免内存中存储完整字节数组
  5. 使用线程安全集合统计成功/失败任务
  6. 严格控制资源关闭逻辑,避免泄漏

内容的提问来源于stack exchange,提问作者Tausif Presolv 360

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 01:02:36