
在 java 应用中,处理大量数据或执行复杂操作时,outofmemoryerror: java heap space 是一个常见的运行时错误。对于 itext 库进行 pdf 合并的场景,尤其当需要合并的文件数量较多或单个文件体积较大时,如果处理不当,极易耗尽 jvm 堆内存。
内存消耗的根源
问题的核心在于现有实现中对 ByteArrayOutputStream 的使用。ByteArrayOutputStream 会将所有写入的数据缓存在内存中,直到调用 toByteArray() 方法时,它会返回一个包含所有数据的字节数组。这意味着,如果合并后的 PDF 文件大小为 100MB,那么在内存中就会存在一个至少 100MB 的字节数组。当需要合并的 PDF 总大小超出 JVM 分配的堆内存限制时,就会抛出 OutOfMemoryError。尽管原始代码中对 PdfReader 进行了及时的 freeReader 和 close 操作,但最终合并结果的内存占用依然是瓶颈。
原始代码示例:
public static byte[] mergePdf(List < InputStream > inputStreams) {
Document document = new Document();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); // 内存瓶颈
PdfCopy copy = new PdfSmartCopy(document, byteArrayOutputStream);
document.open();
for (InputStream inputStream: inputStreams) {
PdfReader pdfReader = new PdfReader(inputStream);
copy.addDocument(pdfReader);
copy.freeReader(pdfReader);
pdfReader.close();
}
document.close();
return byteArrayOutputStream.toByteArray(); // 最终将所有数据加载到内存
}优化策略:直接流式输出
解决 OutOfMemoryError 的关键在于避免在内存中一次性持有整个合并后的 PDF 文件。最佳实践是采用流式处理,直接将合并结果写入到最终的目标 OutputStream。这意味着,合并过程中的数据会直接从输入流读取,经过 iText 处理后,直接写入到指定的输出流,而不会在 JVM 堆内存中累积一个完整的 PDF 字节数组。
这种方法尤其适用于以下场景:
- Web 应用中直接响应下载: 将合并后的 PDF 直接写入 HTTP 响应的 OutputStream,用户可以直接下载,无需在服务器端保存文件或将其完全加载到内存。
- 直接写入文件: 将合并后的 PDF 直接写入 FileOutputStream,避免内存中转。
- 与其他流式处理集成: 与其他需要 OutputStream 作为输入的组件集成。
改进后的代码示例
为了实现直接流式输出,我们需要修改 mergePdf 方法的签名,使其接受一个 OutputStream 参数,并将 PdfCopy 的目标指向这个传入的 OutputStream。
import com.itextpdf.text.Document;
import com.itextpdf.text.pdf.PdfCopy;
import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.PdfSmartCopy;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;
import java.util.List;
public class PdfMergeUtil {
/**
* 合并多个PDF输入流到一个指定的输出流中,避免内存溢出。
*
* @param inputStreams 包含待合并PDF内容的输入流列表。
* @param outputStream 目标输出流,合并后的PDF将直接写入此流。
* @throws IOException 如果在读写PDF时发生IO错误。
*/
public static void mergePdf(List inputStreams, OutputStream outputStream) throws IOException {
Document document = new Document();
PdfCopy copy = null; // 使用PdfCopy或PdfSmartCopy
try {
// PdfSmartCopy 优化了对相似资源的共享,可以减小最终文件大小
copy = new PdfSmartCopy(document, outputStream);
document.open();
for (InputStream inputStream : inputStreams) {
PdfReader pdfReader = null;
try {
pdfReader = new PdfReader(inputStream);
copy.addDocument(pdfReader);
// 释放PdfReader资源,防止内存泄漏
copy.freeReader(pdfReader);
} finally {
// 确保PdfReader和其底层InputStream被关闭
if (pdfReader != null) {
pdfReader.close();
}
if (inputStream != null) {
inputStream.close(); // 关闭传入的InputStream
}
}
}
} finally {
// 确保Document和PdfCopy被关闭,完成PDF写入
if (document.isOpen()) {
document.close();
}
// 不需要关闭传入的outputStream,由调用者负责
}
}
// 示例用法:
public static void main(String[] args) {
// 假设这里有一些InputStream代表PDF文件
List pdfInputStreams = List.of(
// new FileInputStream("path/to/file1.pdf"),
// new FileInputStream("path/to/file2.pdf")
// ... 实际应用中替换为真实InputStream
);
// 示例1: 将合并结果写入文件
// try (OutputStream fos = new FileOutputStream("merged_output.pdf")) {
// mergePdf(pdfInputStreams, fos);
// System.out.println("PDFs merged to merged_output.pdf successfully!");
// } catch (IOException e) {
// e.printStackTrace();
// }
// 示例2: 在Web应用中直接写入HttpServletResponse的OutputStream
// 在Servlet或Spring Controller中:
// response.setContentType("application/pdf");
// response.setHeader("Content-Disposition", "attachment; filename=\"merged.pdf\"");
// try (OutputStream os = response.getOutputStream()) {
// mergePdf(pdfInputStreams, os);
// } catch (IOException e) {
// e.printStackTrace();
// }
}
} 注意事项与最佳实践
- 资源管理: 在上述改进后的代码中,使用了 try-finally 块来确保 Document 和 PdfReader 等 iText 资源的正确关闭。特别是 PdfReader,即使在循环内部也要确保其被关闭,以释放文件句柄和内存。同时,传入的 InputStream 也应在处理完毕后关闭。
- OutputStream 的生命周期: mergePdf 方法不负责关闭传入的 OutputStream。这是因为 OutputStream 的生命周期通常由调用者管理(例如,在 Web 应用中,HttpServletResponse 的 OutputStream 由容器管理;写入文件时,FileOutputStream 应在 try-with-resources 语句中自动关闭)。
- 错误处理: 在实际应用中,应根据业务需求对 IOException 进行更细致的捕获和处理。
- PdfCopy vs. PdfSmartCopy: 示例中使用了 PdfSmartCopy,它是 PdfCopy 的一个子类,可以智能地识别并共享重复的资源(如字体、图片),从而减小最终 PDF 文件的大小。对于大量包含相似内容的 PDF 文件合并,PdfSmartCopy 表现更优。
- JVM 堆内存调整: 尽管直接流式输出能极大缓解 OutOfMemoryError,但如果合并的文件数量极其庞大或单个文件异常复杂,JVM 堆内存仍然可能成为瓶颈。在这种情况下,适当增加 JVM 的堆内存(例如,通过 -Xmx 参数)可以作为辅助手段,但应优先考虑代码层面的内存优化。
总结
通过将 iText PDF 合并的输出从 ByteArrayOutputStream 切换到直接流式写入目标 OutputStream,我们可以有效地避免因合并大文件而导致的 OutOfMemoryError。这种策略不仅提升了应用的内存效率和稳定性,也使得合并后的 PDF 能够更灵活地被处理,例如直接作为 HTTP 响应返回给客户端,或直接写入文件,无需在服务器内存中进行昂贵的完整副本存储。在开发涉及大量 PDF 操作的应用时,这种内存优化是至关重要的。










