问题报错
报错的图片:
看到明显的错误占用了很大的内存
代码块如下
PDFRenderer pdfRenderer = new PDFRenderer(pd);
for (int i = 0; i < pd.getNumberOfPages(); i++) {
BufferedImage image = pdfRenderer.renderImageWithDPI(i, 300, ImageType.RGB);
String name = file.getOriginalFilename().substring(0, file.getOriginalFilename().lastIndexOf("."));
//获取name下划线最后一个前面的值
name = name.substring(0, name.lastIndexOf("_"));
Path tempFile = Files.createTempFile(name, ".jpg");
ImageIO.write(image, "jpg", tempFile.toFile());
// 上传 OSS
String imageUrl = uploadToOSS(tempFile, name, i + 1);
imageUrls.add(imageUrl);
// 清理临时文件
Files.deleteIfExists(tempFile);
}
return imageUrls;
分析问题
一、可能存在的问题
- 内存占用方面
大文件处理:当处理大型 PDF 文件(包含大量页面或高分辨率图像)时,BufferedImage对象会占用大量内存。在循环中,每一页都被渲染成BufferedImage,如果这些图像没有及时释放内存,可能会导致内存占用过高。
临时文件处理:虽然代码中有清理临时文件的操作Files.deleteIfExists(tempFile);,但是如果在uploadToOSS(tempFile, name, i + 1);步骤中出现异常,临时文件可能无法被正确删除,导致磁盘空间占用和潜在的资源泄漏。 - 异常处理方面
在PDDocument.load(file.getInputStream())和ImageIO.write(image, “jpg”, tempFile.toFile())等操作中,如果出现异常,可能会导致PDDocument和其他相关资源没有被正确关闭和释放,进一步引发资源泄漏和潜在的内存问题。 - 性能方面
反复创建和删除临时文件可能会对磁盘 I/O 性能产生影响,尤其是在处理大量 PDF 文件时。
二、引发 OOM(Out - Of - Memory)的原因
- 图像数据积累
在循环中,pdfRenderer.renderImageWithDPI(i, 300, ImageType.RGB);会将 PDF 页面渲染成BufferedImage。如果 PDF 文件有很多页,这些BufferedImage对象会一直存在于内存中(直到方法结束或者垃圾回收),因为它们被存储在imageUrls列表中(通过后续的uploadToOSS操作,但在上传操作完成前内存中仍持有这些图像数据),可能导致内存耗尽。 - 垃圾回收不及时
如果 Java 虚拟机的垃圾回收机制不能及时回收这些大的BufferedImage对象,随着处理的页面越来越多,可用内存会逐渐减少,最终导致 OOM。
避免 OOM 的方法
- 优化内存管理
及时释放图像资源:在将图像上传到 OSS 后,显式地释放BufferedImage所占用的内存。可以通过将BufferedImage相关数据置为null来提示垃圾回收器回收内存,例如image = null;在uploadToOSS(tempFile, name, i + 1);之后。 - 调整 JVM 内存参数:
根据处理的 PDF 文件的大致规模,适当增加 Java 虚拟机的堆内存大小(通过 - Xmx参数),但这只是一种缓解措施,不能从根本上解决内存泄漏问题。 - 改进文件处理
使用内存映射文件(如果适用):对于大型 PDF 文件,可以考虑使用内存映射文件技术来减少内存占用。例如,在加载 PDF 文件时,查看是否有支持内存映射的 PDF 库或方法,这样可以让操作系统更高效地管理文件数据在内存中的加载和卸载。 - 优化临时文件管理:
确保在任何情况下临时文件都能被正确删除。可以使用try - with - resources语句来确保PDDocument等资源被正确关闭,并且在uploadToOSS操作周围添加try - catch - finally块来保证临时文件的删除。
try (PDDocument pd = PDDocument.load(file.getInputStream())){
PDFRenderer pdfRenderer = new PDFRenderer(pd);
for (int i = 0; i < pd.getNumberOfPages(); i++) {
try {
BufferedImage image = pdfRenderer.renderImageWithDPI(i, 300, ImageType.RGB);
String name = file.getOriginalFilename().substring(0, file.getOriginalFilename().lastIndexOf("."));
name = name.substring(0, name.lastIndexOf("_"));
Path tempFile = Files.createTempFile(name, ".jpg");
try {
ImageIO.write(image, "jpg", tempFile.toFile());
String imageUrl = uploadToOSS(tempFile, name, i + 1);
imageUrls.add(imageUrl);
} finally {
Files.deleteIfExists(tempFile);
}
} catch (Exception e) {
// 处理图像渲染和临时文件写入异常
}
}
return imageUrls;
} catch (Exception e) {
// 处理PDDocument加载异常
throw new RuntimeException(e);
}
- 分页处理和流式处理(如果可行)
分页处理:如果可能,将 PDF 文件分页处理,而不是一次性处理所有页面。例如,可以提供一个机制让用户选择一次处理多少页,处理完一批后释放内存,再处理下一批。
流式处理:查看是否有支持流式处理 PDF 文件的库或方法,这样可以边读取边处理,避免将整个文件数据加载到内存中。
思考:如何释放 PDDocument 占用的内存?
有很多方法吧
比如:
- 使用临时文件:当处理大型PDF文件时,可以配置PDFBox使用临时文件来减少内存使用。这可以通过MemoryUsageSetting.setupTempFileOnly()方法实现,这样PDF文件将临时存放在硬盘上,而不是完全加载到内存中。
PDDocument document = PDDocument.load(new File("path/to/document.pdf"), MemoryUsageSetting.setupTempFileOnly());
- 关闭PDDocument对象:确保在不再需要PDDocument对象时关闭它,以释放与之关联的资源。
document.close();
- 减少渲染图像的分辨率:在渲染图像时,可以通过降低DPI值或缩小图像尺寸来减少内存使用。
PDFRenderer renderer = new PDFRenderer(document);
renderer.setSubsamplingAllowed(true); // 允许下采样
BufferedImage image = renderer.renderImage(pageIndex, 100); // 使用较低的DPI值
-
优化内存设置:调整JVM的内存设置,例如增加最大堆内存-Xmx值,可以为处理大型文件提供更多内存空间。
-
自定义资源缓存:通过自定义DefaultResourceCache,可以减少缓存中的资源占用,从而降低内存使用。
-
处理完立即释放图像资源:在渲染图像后,及时释放不再需要的图像资源,避免长时间持有这些资源。
-
增量加载PDF:如果可能,使用增量加载方法loadNonSeq,这样可以减少初始内存占用,因为只有PDF的部分内容被加载到内存中。
-
清理缓存:在Linux系统中,可以通过写入/proc/sys/vm/drop_caches来清除缓存,释放内存。这可以通过定时任务来实现。
-
更新PDFBox版本:确保使用的是最新版本的PDFBox,因为新版本可能包含内存优化和bug修复。
-
优化PDF文件:在处理前,尝试优化PDF文件,例如通过减小图像大小或移除不必要的内容,以减少处理时的内存需求
朋友们,你有遇到OOM异常吗?如何解决的?