将大量数据写入 excel:超出 GC 开销限制

Posted

技术标签:

【中文标题】将大量数据写入 excel:超出 GC 开销限制【英文标题】:Writing large of data to excel: GC overhead limit exceeded 【发布时间】:2018-03-02 10:11:14 【问题描述】:

我有一个从 MongoDB 读取的字符串列表(约 20 万行) 然后我想用Java代码将它写入一个excel文件:

public class OutputToExcelUtils 

    private static XSSFWorkbook workbook;
    private static final String DATA_SEPARATOR = "!";

    public static void clusterOutToExcel(List<String> data, String outputPath) 

        workbook = new XSSFWorkbook();
        FileOutputStream outputStream = null;

        writeData(data, "Data");


        try 
            outputStream = new FileOutputStream(outputPath);            
            workbook.write(outputStream);
            workbook.close();
         catch (IOException e) 
            e.printStackTrace();
        
    

    public static void writeData(List<String> data, String sheetName) 

        int rowNum = 0;
        XSSFSheet sheet = workbook.getSheet(sheetName);     
        sheet = workbook.createSheet(sheetName);


        for (int i = 0; i < data.size(); i++) 
            System.out.println(sheetName + " Processing line: " + i);
            int colNum = 0;
            // Split into value of cell
            String[] valuesOfLine = data.get(i).split(DATA_SEPERATOR);

            Row row = sheet.createRow(rowNum++);

            for (String valueOfCell : valuesOfLine) 
                Cell cell = row.createCell(colNum++);
                cell.setCellValue(valueOfCell);
            
        
    


然后我得到一个错误:

线程“主”java.lang.OutOfMemoryError 中的异常:GC 开销 超出限制 org.apache.xmlbeans.impl.store.Cur$Locations.(Cur.java:497) 在 org.apache.xmlbeans.impl.store.Locale.(Locale.java:168) 在 org.apache.xmlbeans.impl.store.Locale.getLocale(Locale.java:242) 在 org.apache.xmlbeans.impl.store.Locale.newInstance(Locale.java:593) 在 org.apache.xmlbeans.impl.schema.SchemaTypeLoaderBase.newInstance(SchemaTypeLoaderBase.java:198) 在 org.apache.poi.POIXMLTypeLoader.newInstance(POIXMLTypeLoader.java:132) 在 org.openxmlformats.schemas.spreadsheetml.x2006.main.CTRst$Factory.newInstance(未知 来源)在 org.apache.poi.xssf.usermodel.XSSFRichTextString.(XSSFRichTextString.java:87) 在 org.apache.poi.xssf.usermodel.XSSFCell.setCellValue(XSSFCell.java:417) 在 ups.mongo.excelutil.OutputToExcelUtils.writeData(OutputToExcelUtils.java:80) 在 ups.mongo.excelutil.OutputToExcelUtils.clusterOutToExcel(OutputToExcelUtils.java:30) 在 ups.mongodb.App.main(App.java:74)

请给我一些建议?

非常感谢您。

更新解决方案:使用 SXSSWorkbook 代替 XSSWorkbook

public class OutputToExcelUtils 

    private static SXSSFWorkbook workbook;
    private static final String DATA_SEPERATOR = "!";

    public static void clusterOutToExcel(ClusterOutput clusterObject, ClusterOutputTrade clusterOutputTrade,
            ClusterOutputDistance ClusterOutputDistance, String outputPath) 

        workbook = new SXSSFWorkbook();
        workbook.setCompressTempFiles(true);
        FileOutputStream outputStream = null;

        writeData(clusterOutputTrade.getTrades(), "Data");

        try 
            outputStream = new FileOutputStream(outputPath);            
            workbook.write(outputStream);
            workbook.close();
         catch (IOException e) 
            e.printStackTrace();
        
    

    public static void writeData(List<String> data, String sheetName) 

        int rowNum = 0;
        SXSSFSheet sheet = workbook.createSheet(sheetName);
        sheet.setRandomAccessWindowSize(100); // For 100 rows saved in memory, it will flushed after wirtten to excel file

        for (int i = 0; i < data.size(); i++) 
            System.out.println(sheetName + " Processing line: " + i);
            int colNum = 0;
            // Split into value of cell
            String[] valuesOfLine = data.get(i).split(DATA_SEPERATOR);

            Row row = sheet.createRow(rowNum++);

            for (String valueOfCell : valuesOfLine) 
                Cell cell = row.createCell(colNum++);
                cell.setCellValue(valueOfCell);
            
        
    


【问题讨论】:

如果可能,不要一次将所有数据保存在内存中。否则让你的 JVM 使用更多内存。 你需要excel文件吗?你能生成可以在大多数电子表格中打开的 tsv/csv 文件吗? 谢谢亨利,你的想法和下面的亚历克斯一样。因为excel文件有mant sheet所以不能生成为CSV文件。 在this SO article 的评论中提到:“SXSSF 分配临时文件,您必须始终通过调用 dispose 方法明确清理。 请参阅 SXSSF documentation.” 【参考方案1】:

您的应用程序在垃圾收集上花费了太多时间。这并不一定意味着堆空间不足。但是,相对于执行实际工作,它在 GC 上花费了太多时间,因此 Java 运行时将其关闭。

尝试使用以下 JVM 选项启用吞吐量收集

-XX:+UseParallelGC

当您使用它时,请为您的应用程序提供尽可能多的堆空间:

-Xms????m

(其中???? 代表堆空间量,以 MB 为单位,例如-Xms8192m

如果这没有帮助,请尝试使用此选项设置更宽松的吞吐量目标:

-XX:GCTimeRatio=19 

这指定您的应用程序应该比 GC 相关的工作多做 19 倍的有用工作,即它允许 GC 消耗多达 5% 的处理器时间(我相信更严格的 1% 默认目标可能会导致上述情况运行时错误)

不保证他会奏效。您能否检查并回帖,以便遇到类似问题的其他人受益?

编辑

您的根本问题仍然是您需要在构建时将整个电子表格及其所有相关对象保存在内存中。另一种解决方案是序列化数据,即编写实际的电子表格文件,而不是在内存中构造它并在最后保存。但是,这需要阅读 XLXS 格式并创建自定义解决方案。

另一种选择是寻找内存密集度较低的库(如果存在的话)。 POI 的可能替代品是 JExcelAPI(开源)和 Aspose.Cells(商业)。

我多年前使用过 JExcelAPI,并获得了积极的体验(但是,它的维护似乎不如 POI 积极,因此可能不再是最佳选择)。

编辑 2

看起来 POI 提供了流模型 (https://poi.apache.org/spreadsheet/how-to.html#sxssf),所以这可能是最好的整体方法。

【讨论】:

谢谢托尼,我会尝试这种方式。但是似乎有人说增加堆空间来解决这个问题不是一个好主意。 @Hana 问题出在内存中没有保存 200,000 行文本;问题在于电子表格及其所有相关对象,它们消耗的内存量是源数据内存量的 10-100 倍——我会用其他一些想法来修改我的答案 是的,我认为我们应该在数据写入 excel 后刷新数据。所以,内存不会把它放在内存中。你可以看看这张图片:i.stack.imgur.com/ZnYda.png XSSF 的另一个版本是 SXSSF 可以更高效地工作。 您已向我 +1,因为您已经确定了 GC 的实际原因。哪个是在内存中保存其状态的 excel 库。我认为尽管答案对于实际解决方案来说太大了。我认为这里不需要 GC 调整选项。【参考方案2】:

尽量不要将所有数据加载到内存中。即使 200k 行的二进制表示不是那么大,内存中的隐藏对象也可能太大。就像你有一个 Pojo 的提示一样,这个 pojo 中的每个属性都有一个指针,每个指针取决于它是否被压缩,将占用 4 或 8 个字节。这意味着,如果您的数据是仅针对指针的具有 4 个属性的 Pojo,您将花费 200 000* 4 字节(或 8 字节)。

理论上,您可以增加 JVM 的内存量,但这不是一个好的解决方案,或者更准确地说,它对于 Live 系统来说不是一个好的解决方案。对于非交互式系统可能没问题。

提示:使用 -Xmx -Xms jvm 参数来控制堆大小。

【讨论】:

谢谢你,亚历克斯,我看到你的回答有 2 点。 1.数据我没有使用POJO,它们是从MongoDB加载的纯文本,所以我不控制属性。 2.正如你所说,增加JVM的内存不是一个好主意。系统将从用户那里获取交互。所以这可能不是一个好的解决方案。 是的,我试图向您解释为什么在堆中创建的对象实际上消耗的内存比它们在硬盘上消耗的内存要多得多。以及为什么你不应该尝试通过翻转堆来解决这个问题。 谢谢亚历克斯,我明白了!我试图加载一小部分数据以避免 JVM 过载。但是还是不行。 这个总体思路不错,但是OP的问题是GC过多造成的,不一定是内存不足。但是 -Xms 对于内存密集型应用程序来说总是一个好主意,所以 +1【参考方案3】:

不是从数据中获取整个列表,而是逐行迭代。 如果太麻烦,将列表写入文件,然后逐行重新读取,例如Stream&lt;String&gt;

  Path path = Files.createTempFile(...);
  Files.write(path, list, StandarCharsets.UTF_8);
  Files.lines(path, StandarCharsets.UTF_8)
     .forEach(line ->  ... );

在 Excel 方面:虽然 xlsx 使用共享字符串,但如果 XSSF 操作不小心, 以下将对重复的字符串值使用单个 String 实例。

public class StringCache 
    private static final int MAX_LENGTH = 40;
    private Map<String, String> identityMap = new Map<>();

    public String cached(String s) 
         if (s == null) 
             return null;
         
         if (s.length() > MAX_LENGTH) 
             return s;
         
         String t = identityMap.get(s);
         if (t == null) 
             t = s;
             identityMap.put(t, t);
         
         return t;
    


StringCache strings = new StringCache();

       for (String valueOfCell : valuesOfLine) 
            Cell cell = row.createCell(colNum++);
            cell.setCellValue(strings.cached(valueOfCell));
       

【讨论】:

new Map&lt;&gt;();? "a;a;a;a".split(";") 将创建 4 个 String 对象。我的缓存会将其减少到 1。但是请注意,如果结果像 "&lt;c:v&gt;" + a + "&lt;/c:v&gt;" 一样连接起来,它不需要在这里工作。

以上是关于将大量数据写入 excel:超出 GC 开销限制的主要内容,如果未能解决你的问题,请参考以下文章

将大型数据集缓存到 spark 内存中时“超出 GC 开销限制”(通过 sparklyr 和 RStudio)

跟踪“超出 GC 开销限制”错误

超出 Java GC 开销限制 - 需要自定义解决方案

Spark DataFrame java.lang.OutOfMemoryError:长循环运行时超出了GC开销限制

Java jdbcpreparedStatement 超出了 OutOfMemoryError GC 开销限制

优化 Hive 查询。 java.lang.OutOfMemoryError:超出 Java 堆空间/GC 开销限制