使用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();
总结优化后的流程
- 预解析所有共用图片资源为
Image对象并缓存 - 全局初始化并复用
ConverterProperties与编译后的模板 - 使用自定义线程池控制并发数与队列大小
- 生成PDF时优先写入临时文件,避免内存中存储完整字节数组
- 使用线程安全集合统计成功/失败任务
- 严格控制资源关闭逻辑,避免泄漏
内容的提问来源于stack exchange,提问作者Tausif Presolv 360
相关产品推荐
相关产品推荐

