Apache POI WorkbookFactory.create抛出java.lang.OutOfMemoryError: Java heap space异常

3
我的问题很简单。我想在App Engine中验证大小为50MB的文件是否格式正确。
这现在带来了许多大的挑战。首先是Apache XLS/XLSX POI API。当我将20MB的文件数据在本地加载到内存中进行验证时,它会抛出异常:
java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Arrays.java:2271)
    at java.io.ByteArrayOutputStream.grow(ByteArrayOutputStream.java:113)
    at java.io.ByteArrayOutputStream.ensureCapacity(ByteArrayOutputStream.java:93)
    at java.io.ByteArrayOutputStream.write(ByteArrayOutputStream.java:140)
    at org.apache.poi.openxml4j.util.ZipInputStreamZipEntrySource$FakeZipEntry.<init>(ZipInputStreamZipEntrySource.java:128)
    at org.apache.poi.openxml4j.util.ZipInputStreamZipEntrySource.<init>(ZipInputStreamZipEntrySource.java:55)
    at org.apache.poi.openxml4j.opc.ZipPackage.<init>(ZipPackage.java:84)
    at org.apache.poi.openxml4j.opc.OPCPackage.open(OPCPackage.java:272)
    at org.apache.poi.ss.usermodel.WorkbookFactory.create(WorkbookFactory.java:79)

我需要打开和验证大小为20到25MB的电子表格。如果可能的话,50MB是一个不错的目标。我们要处理单个工作表上数十万行数据。
现在我的传统代码将整个文件加载到内存中,然后立即导致我的应用引擎实例崩溃。以下是我的传统代码:
    public ErrorLog validateWorkbook(inputWorkbook)
    {
        int sheetCount = inputWorkbook.getNumberOfSheets();
        for (int x = 0; x< sheetCount; x++)
        {
            Sheet currentSheet = inputWorkbook.getSheetAt(x);
            Iterator<Row> rowIterator = currentSheet.rowIterator();
            while(rowIterator.hasNext())
            {
                Iterator<Cell> cellIterator = rowIterator.next().cellIterator();
                while(cellIterator.hasNext())
                {
                    Cell currentCell = cellIterator.next();
                    boolean success = validateCellContents(currentCell);
                    if(!success)
                        ErrorLog.appendError(new Error()); // detailed user error explicitly defining error location, cell value, and recommended steps to fix
                }
            }
        }
        return ErrorLog;
    }

现在,据说有基于事件的方法来处理每次遇到单元格时的动作监听器。但是虚拟代码 here 引用了:
ReadOnlySharedStringsTable strings = new ReadOnlySharedStringsTable(container); 

我在调试器中检查了这个对象,并且它包含当前工作表中每个唯一字符串引用。这本质上正是我试图避免的。它会预先分配一大块内存来存储每个值。理想的解决方案需要获取输入字节流并在遍历文件时解码字符串,以减少内存占用。
因为字符串表肯定会占用大量内存空间。我正在处理15万到30万行项目电子表格。
现在快速指南提到您可以使用FileInputStream,如果使用File,则输入将被缓冲。问题在于,App Engine和Blob Store服务不知道File对象,而只返回InputStreams(据我所知)。
此外,另一个事件驱动模型 默认处理程序 在其接口定义的方法中似乎没有针对每个值的列或行概念(并且它还会预先分配整个共享字符串表)。
我已经没有更多想法了!我将尝试为此提供赏金。至少一个明确的“不可能”就足够了,然后我可以开始寻找解决方法,但我感觉我没有充分利用庞大的API。

我已经修改了我的eclipse.ini文件以增加内存,但服务器仍然很快出现错误。我知道如何增加Tomcat实例的内存,但不知道如何增加本地应用程序引擎服务器实例的内存。我相信这就是错误发生的地方。 - fIwJlxSzApHEZIl
3
如果内存是一个问题,为什么不使用XSSF流式SAX读取API而不是常规的XSSF用户模型?前者是流式和低内存,后者更容易使用但类似于DOM,所以内存更高。 - Gagravarr
1
这看起来不错,但比其他API复杂得多!感谢提供链接,我明天上班会查看它! - fIwJlxSzApHEZIl
1
Apache POI附带了使用XSSF事件API和HSSF事件API的几个很好的示例,我只能建议您将代码与这些示例进行比较,并查看您所做的不同之处。正如示例所示,完全可以在单元格经过时获取其值,以及发现“缺失”的单元格! - Gagravarr
1
XSSFEventBasedExcelExtractor 是另一个建议。每个单元格的 XML 元素都带有单元格引用作为属性,从中可以获取行和列,或者您可以自己跟踪。 - Gagravarr
显示剩余4条评论
1个回答

1

倡导者,

可以做到这一点,但是你需要创造性地绕过GAE的一些限制。

首先,App Engine前端实例有一个1分钟的请求限制,因此如果您想处理50 MB大小的文件,您将被迫使用任务队列或使用“手动/基本缩放模块”来避免时间限制。

其次,内存。在这里,您有两个选择,使用模块,您可以更好地控制实例的内存,这是向正确方向迈出的一步,但它不会扩展得很好。

我曾经处于你的情况,最终使用了Google Drive APIGoogle Spreadheets APIBlobstore service,具体取决于需求。使用这些替代方案之一,我上传了Excel文件,以便可以使用队列离线批量处理它们。

感谢 @jirungaray 的精彩回答!!不幸的是,我已经不在这个项目上了。但是我会记住你的解决方案,为未来的机会做好准备,并将其标记为正确答案 :)我认为关键在于上传电子表格,将其分解并分块处理,以避免实例因内存不足而停止运行,正如你所说的那样。 - fIwJlxSzApHEZIl

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接